idiomatic project structure
This commit is contained in:
20
internal/api/db_interface.go
Normal file
20
internal/api/db_interface.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"go-nkode/internal/models"
|
||||
)
|
||||
|
||||
type DbAccessor interface {
|
||||
GetCustomer(models.CustomerId) (*models.Customer, error)
|
||||
GetUser(models.UserEmail, models.CustomerId) (*models.User, error)
|
||||
WriteNewCustomer(models.Customer) error
|
||||
WriteNewUser(models.User) error
|
||||
UpdateUserNKode(models.User) error
|
||||
UpdateUserInterface(models.UserId, models.UserInterface) error
|
||||
UpdateUserRefreshToken(models.UserId, string) error
|
||||
Renew(models.CustomerId) error
|
||||
RefreshUserPasscode(models.User, []int, models.CustomerAttributes) error
|
||||
RandomSvgInterface(models.KeypadDimension) ([]string, error)
|
||||
RandomSvgIdxInterface(models.KeypadDimension) (models.SvgIdInterface, error)
|
||||
GetSvgStringInterface(models.SvgIdInterface) ([]string, error)
|
||||
}
|
||||
444
internal/api/handler.go
Normal file
444
internal/api/handler.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/models"
|
||||
"go-nkode/internal/security"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NKodeHandler struct {
|
||||
Api NKodeAPI
|
||||
}
|
||||
|
||||
const (
|
||||
CreateNewCustomer = "/create-new-customer"
|
||||
GenerateSignupResetInterface = "/generate-signup-reset-interface"
|
||||
SetNKode = "/set-nkode"
|
||||
ConfirmNKode = "/confirm-nkode"
|
||||
GetLoginInterface = "/get-login-interface"
|
||||
Login = "/login"
|
||||
RenewAttributes = "/renew-attributes"
|
||||
RandomSvgInterface = "/random-svg-interface"
|
||||
RefreshToken = "/refresh-token"
|
||||
ResetNKode = "/reset-nkode"
|
||||
)
|
||||
|
||||
const (
|
||||
malformedCustomerId = "malformed customer id"
|
||||
malformedUserEmail = "malformed user email"
|
||||
malformedSessionId = "malformed session id"
|
||||
invalidKeypadDimensions = "invalid keypad dimensions"
|
||||
)
|
||||
|
||||
func (h *NKodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case CreateNewCustomer:
|
||||
h.CreateNewCustomerHandler(w, r)
|
||||
case GenerateSignupResetInterface:
|
||||
h.GenerateSignupResetInterfaceHandler(w, r)
|
||||
case SetNKode:
|
||||
h.SetNKodeHandler(w, r)
|
||||
case ConfirmNKode:
|
||||
h.ConfirmNKodeHandler(w, r)
|
||||
case GetLoginInterface:
|
||||
h.GetLoginInterfaceHandler(w, r)
|
||||
case Login:
|
||||
h.LoginHandler(w, r)
|
||||
case RenewAttributes:
|
||||
h.RenewAttributesHandler(w, r)
|
||||
case RandomSvgInterface:
|
||||
h.RandomSvgInterfaceHandler(w, r)
|
||||
case RefreshToken:
|
||||
h.RefreshTokenHandler(w, r)
|
||||
case ResetNKode:
|
||||
h.ResetNKode(w, r)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err := w.Write([]byte("404 not found"))
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewCustomerHandler handles the creation of a new customer.
|
||||
// @Summary Create a new customer
|
||||
// @Description Creates a new customer based on the provided policy information.
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param NewCustomerPost body NewCustomerPost true "Customer creation data"
|
||||
// @Success 200 {object} CreateNewCustomerResp
|
||||
// @Router /create-new-customer [post]
|
||||
func (h *NKodeHandler) CreateNewCustomerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("create new customer")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
var customerPost models.NewCustomerPost
|
||||
if err := decodeJson(w, r, &customerPost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := h.Api.CreateNewCustomer(customerPost.NKodePolicy, nil)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
respBody := models.CreateNewCustomerResp{
|
||||
CustomerId: uuid.UUID(*customerId).String(),
|
||||
}
|
||||
marshalAndWriteBytes(w, respBody)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) GenerateSignupResetInterfaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("signup/reset interface")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
var signupResetPost models.GenerateSignupRestInterfacePost
|
||||
if err := decodeJson(w, r, &signupResetPost); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kp := models.KeypadDimension{
|
||||
AttrsPerKey: signupResetPost.AttrsPerKey,
|
||||
NumbOfKeys: signupResetPost.NumbOfKeys,
|
||||
}
|
||||
if err := kp.IsValidKeypadDimension(); err != nil {
|
||||
badRequest(w, invalidKeypadDimensions)
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(signupResetPost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
userEmail, err := models.ParseEmail(signupResetPost.UserEmail)
|
||||
if err != nil {
|
||||
badRequest(w, malformedUserEmail)
|
||||
return
|
||||
}
|
||||
resp, err := h.Api.GenerateSignupResetInterface(userEmail, models.CustomerId(customerId), kp, signupResetPost.Reset)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
marshalAndWriteBytes(w, resp)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) SetNKodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("set nkode")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
var setNKodePost models.SetNKodePost
|
||||
if err := decodeJson(w, r, &setNKodePost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(setNKodePost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
sessionId, err := uuid.Parse(setNKodePost.SessionId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedSessionId)
|
||||
return
|
||||
}
|
||||
confirmInterface, err := h.Api.SetNKode(models.CustomerId(customerId), models.SessionId(sessionId), setNKodePost.KeySelection)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
respBody := models.SetNKodeResp{UserInterface: confirmInterface}
|
||||
marshalAndWriteBytes(w, respBody)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) ConfirmNKodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("confirm nkode")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
var confirmNKodePost models.ConfirmNKodePost
|
||||
if err := decodeJson(w, r, &confirmNKodePost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(confirmNKodePost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
sessionId, err := uuid.Parse(confirmNKodePost.SessionId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedSessionId)
|
||||
return
|
||||
}
|
||||
if err = h.Api.ConfirmNKode(models.CustomerId(customerId), models.SessionId(sessionId), confirmNKodePost.KeySelection); err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) GetLoginInterfaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("get login interface")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
var loginInterfacePost models.GetLoginInterfacePost
|
||||
if err := decodeJson(w, r, &loginInterfacePost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(loginInterfacePost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
userEmail, err := models.ParseEmail(loginInterfacePost.UserEmail)
|
||||
if err != nil {
|
||||
badRequest(w, malformedUserEmail)
|
||||
}
|
||||
loginInterface, err := h.Api.GetLoginInterface(userEmail, models.CustomerId(customerId))
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
marshalAndWriteBytes(w, loginInterface)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("login")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
var loginPost models.LoginPost
|
||||
if err := decodeJson(w, r, &loginPost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(loginPost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
userEmail, err := models.ParseEmail(loginPost.UserEmail)
|
||||
if err != nil {
|
||||
badRequest(w, malformedUserEmail)
|
||||
return
|
||||
}
|
||||
jwtTokens, err := h.Api.Login(models.CustomerId(customerId), userEmail, loginPost.KeySelection)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
marshalAndWriteBytes(w, jwtTokens)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) RenewAttributesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
println("renew attributes")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
var renewAttributesPost models.RenewAttributesPost
|
||||
if err := decodeJson(w, r, &renewAttributesPost); err != nil {
|
||||
return
|
||||
}
|
||||
customerId, err := uuid.Parse(renewAttributesPost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
if err = h.Api.RenewAttributes(models.CustomerId(customerId)); err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) RandomSvgInterfaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("random svg interface")
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w)
|
||||
}
|
||||
svgs, err := h.Api.RandomSvgInterface()
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := models.RandomSvgInterfaceResp{
|
||||
Svgs: svgs,
|
||||
Colors: models.SetColors,
|
||||
}
|
||||
|
||||
marshalAndWriteBytes(w, respBody)
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
println("refresh tokens")
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w)
|
||||
}
|
||||
refreshToken, err := getBearerToken(r)
|
||||
if err != nil {
|
||||
forbidden(w)
|
||||
return
|
||||
}
|
||||
refreshClaims, err := security.ParseRegisteredClaimToken(refreshToken)
|
||||
customerId, err := uuid.Parse(refreshClaims.Issuer)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
userEmail, err := models.ParseEmail(refreshClaims.Subject)
|
||||
if err != nil {
|
||||
badRequest(w, malformedUserEmail)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
accessToken, err := h.Api.RefreshToken(userEmail, models.CustomerId(customerId), refreshToken)
|
||||
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
marshalAndWriteBytes(w, models.RefreshTokenResp{AccessToken: accessToken})
|
||||
}
|
||||
|
||||
func (h *NKodeHandler) ResetNKode(w http.ResponseWriter, r *http.Request) {
|
||||
println("reset nkode")
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
}
|
||||
var resetNKodePost models.ResetNKodePost
|
||||
if err := decodeJson(w, r, &resetNKodePost); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
customerId, err := uuid.Parse(resetNKodePost.CustomerId)
|
||||
if err != nil {
|
||||
badRequest(w, malformedCustomerId)
|
||||
return
|
||||
}
|
||||
|
||||
userEmail, err := models.ParseEmail(resetNKodePost.UserEmail)
|
||||
if err != nil {
|
||||
badRequest(w, malformedUserEmail)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.Api.ResetNKode(userEmail, models.CustomerId(customerId)); err != nil {
|
||||
internalServerError(w)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func decodeJson(w http.ResponseWriter, r *http.Request, post any) error {
|
||||
if r.Body == nil {
|
||||
badRequest(w, "unable to parse body")
|
||||
log.Println("error decoding json: body is nil")
|
||||
return errors.New("body is nil")
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&post)
|
||||
if err != nil {
|
||||
badRequest(w, "unable to parse body")
|
||||
log.Println("error decoding json: ", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func internalServerError(w http.ResponseWriter) {
|
||||
log.Print("500 internal server error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("500 Internal Server Error"))
|
||||
}
|
||||
|
||||
func badRequest(w http.ResponseWriter, msg string) {
|
||||
log.Print("bad request: ", msg)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if msg == "" {
|
||||
w.Write([]byte("400 Bad Request"))
|
||||
} else {
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func methodNotAllowed(w http.ResponseWriter) {
|
||||
log.Print("405 method not allowed")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write([]byte("405 method not allowed"))
|
||||
}
|
||||
|
||||
func forbidden(w http.ResponseWriter) {
|
||||
log.Print("403 forbidden")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("403 Forbidden"))
|
||||
}
|
||||
|
||||
func handleError(w http.ResponseWriter, err error) {
|
||||
log.Print("handling error: ", err)
|
||||
statusCode, exists := config.HttpErrMap[err]
|
||||
if !exists {
|
||||
internalServerError(w)
|
||||
return
|
||||
}
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
badRequest(w, err.Error())
|
||||
case http.StatusForbidden:
|
||||
forbidden(w)
|
||||
case http.StatusInternalServerError:
|
||||
internalServerError(w)
|
||||
default:
|
||||
log.Print("unknown error: ", err)
|
||||
internalServerError(w)
|
||||
}
|
||||
}
|
||||
|
||||
func getBearerToken(r *http.Request) (string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
// Check if the Authorization header is present and starts with "Bearer "
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return "", errors.New("forbidden")
|
||||
}
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func marshalAndWriteBytes(w http.ResponseWriter, data any) {
|
||||
respBytes, err := json.Marshal(data)
|
||||
|
||||
if err != nil {
|
||||
internalServerError(w)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(respBytes)
|
||||
|
||||
if err != nil {
|
||||
internalServerError(w)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
269
internal/api/nkode_api.go
Normal file
269
internal/api/nkode_api.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/email"
|
||||
"go-nkode/internal/models"
|
||||
"go-nkode/internal/security"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionExpiration = 5 * time.Minute
|
||||
sessionCleanupInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
type NKodeAPI struct {
|
||||
Db DbAccessor
|
||||
SignupSessionCache *cache.Cache
|
||||
EmailQueue *email.EmailQueue
|
||||
}
|
||||
|
||||
func NewNKodeAPI(db DbAccessor, queue *email.EmailQueue) NKodeAPI {
|
||||
return NKodeAPI{
|
||||
Db: db,
|
||||
EmailQueue: queue,
|
||||
SignupSessionCache: cache.New(sessionExpiration, sessionCleanupInterval),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) CreateNewCustomer(nkodePolicy models.NKodePolicy, id *models.CustomerId) (*models.CustomerId, error) {
|
||||
newCustomer, err := models.NewCustomer(nkodePolicy)
|
||||
if id != nil {
|
||||
newCustomer.Id = *id
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = n.Db.WriteNewCustomer(*newCustomer)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &newCustomer.Id, nil
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) GenerateSignupResetInterface(userEmail models.UserEmail, customerId models.CustomerId, kp models.KeypadDimension, reset bool) (*models.GenerateSignupResetInterfaceResp, error) {
|
||||
user, err := n.Db.GetUser(userEmail, customerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != nil && !reset {
|
||||
log.Printf("user %s already exists", string(userEmail))
|
||||
return nil, config.ErrUserAlreadyExists
|
||||
}
|
||||
svgIdxInterface, err := n.Db.RandomSvgIdxInterface(kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signupSession, err := models.NewSignupResetSession(userEmail, kp, customerId, svgIdxInterface, reset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//n.SignupSessions[signupSession.Id] = *signupSession
|
||||
if err := n.SignupSessionCache.Add(signupSession.Id.String(), *signupSession, sessionExpiration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svgInterface, err := n.Db.GetSvgStringInterface(signupSession.LoginUserInterface.SvgId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := models.GenerateSignupResetInterfaceResp{
|
||||
UserIdxInterface: signupSession.SetIdxInterface,
|
||||
SvgInterface: svgInterface,
|
||||
SessionId: uuid.UUID(signupSession.Id).String(),
|
||||
Colors: signupSession.Colors,
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) SetNKode(customerId models.CustomerId, sessionId models.SessionId, keySelection models.KeySelection) (models.IdxInterface, error) {
|
||||
_, err := n.Db.GetCustomer(customerId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, exists := n.SignupSessionCache.Get(sessionId.String())
|
||||
if !exists {
|
||||
log.Printf("session id does not exist %s", sessionId)
|
||||
return nil, config.ErrSignupSessionDNE
|
||||
}
|
||||
userSession, ok := session.(models.UserSignSession)
|
||||
if !ok {
|
||||
// handle the case where the type assertion fails
|
||||
return nil, config.ErrSignupSessionDNE
|
||||
}
|
||||
confirmInterface, err := userSession.SetUserNKode(keySelection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.SignupSessionCache.Set(sessionId.String(), userSession, sessionExpiration)
|
||||
return confirmInterface, nil
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) ConfirmNKode(customerId models.CustomerId, sessionId models.SessionId, keySelection models.KeySelection) error {
|
||||
session, exists := n.SignupSessionCache.Get(sessionId.String())
|
||||
if !exists {
|
||||
log.Printf("session id does not exist %s", sessionId)
|
||||
return config.ErrSignupSessionDNE
|
||||
}
|
||||
userSession, ok := session.(models.UserSignSession)
|
||||
if !ok {
|
||||
// handle the case where the type assertion fails
|
||||
return config.ErrSignupSessionDNE
|
||||
}
|
||||
customer, err := n.Db.GetCustomer(customerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passcode, err := userSession.DeducePasscode(keySelection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = customer.IsValidNKode(userSession.Kp, passcode); err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := models.NewUser(*customer, string(userSession.UserEmail), passcode, userSession.LoginUserInterface, userSession.Kp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if userSession.Reset {
|
||||
err = n.Db.UpdateUserNKode(*user)
|
||||
} else {
|
||||
err = n.Db.WriteNewUser(*user)
|
||||
}
|
||||
n.SignupSessionCache.Delete(userSession.Id.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) GetLoginInterface(userEmail models.UserEmail, customerId models.CustomerId) (*models.GetLoginInterfaceResp, error) {
|
||||
user, err := n.Db.GetUser(userEmail, customerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
log.Printf("user %s for customer %s dne", userEmail, customerId)
|
||||
return nil, config.ErrUserForCustomerDNE
|
||||
}
|
||||
err = user.Interface.PartialInterfaceShuffle()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = n.Db.UpdateUserInterface(user.Id, user.Interface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svgInterface, err := n.Db.GetSvgStringInterface(user.Interface.SvgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := models.GetLoginInterfaceResp{
|
||||
UserIdxInterface: user.Interface.IdxInterface,
|
||||
SvgInterface: svgInterface,
|
||||
NumbOfKeys: user.Kp.NumbOfKeys,
|
||||
AttrsPerKey: user.Kp.AttrsPerKey,
|
||||
Colors: models.SetColors,
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) Login(customerId models.CustomerId, userEmail models.UserEmail, keySelection models.KeySelection) (*security.AuthenticationTokens, error) {
|
||||
customer, err := n.Db.GetCustomer(customerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := n.Db.GetUser(userEmail, customerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
log.Printf("user %s for customer %s dne", userEmail, customerId)
|
||||
return nil, config.ErrUserForCustomerDNE
|
||||
}
|
||||
passcode, err := models.ValidKeyEntry(*user, *customer, keySelection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Renew {
|
||||
err = n.Db.RefreshUserPasscode(*user, passcode, customer.Attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
jwtToken, err := security.NewAuthenticationTokens(string(user.Email), uuid.UUID(customerId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = n.Db.UpdateUserRefreshToken(user.Id, jwtToken.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &jwtToken, nil
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) RenewAttributes(customerId models.CustomerId) error {
|
||||
return n.Db.Renew(customerId)
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) RandomSvgInterface() ([]string, error) {
|
||||
return n.Db.RandomSvgInterface(models.KeypadMax)
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) RefreshToken(userEmail models.UserEmail, customerId models.CustomerId, refreshToken string) (string, error) {
|
||||
user, err := n.Db.GetUser(userEmail, customerId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user == nil {
|
||||
log.Printf("user %s for customer %s dne", userEmail, customerId)
|
||||
return "", config.ErrUserForCustomerDNE
|
||||
}
|
||||
if user.RefreshToken != refreshToken {
|
||||
return "", config.ErrRefreshTokenInvalid
|
||||
}
|
||||
refreshClaims, err := security.ParseRegisteredClaimToken(refreshToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = security.ClaimExpired(*refreshClaims); err != nil {
|
||||
return "", err
|
||||
}
|
||||
newAccessClaims := security.NewAccessClaim(string(userEmail), uuid.UUID(customerId))
|
||||
return security.EncodeAndSignClaims(newAccessClaims)
|
||||
}
|
||||
|
||||
func (n *NKodeAPI) ResetNKode(userEmail models.UserEmail, customerId models.CustomerId) error {
|
||||
user, err := n.Db.GetUser(userEmail, customerId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user in rest nkode %v", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nkodeResetJwt, err := security.ResetNKodeToken(string(userEmail), uuid.UUID(customerId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
frontendHost := os.Getenv("FRONTEND_HOST")
|
||||
if frontendHost == "" {
|
||||
frontendHost = config.FrontendHost
|
||||
}
|
||||
htmlBody := fmt.Sprintf("<h1>Hello!</h1><p>Click the link to reset your nKode.</p><a href=\"%s?token=%s\">Reset nKode</a>", frontendHost, nkodeResetJwt)
|
||||
email := email.Email{
|
||||
Sender: "no-reply@nkode.tech",
|
||||
Recipient: string(userEmail),
|
||||
Subject: "nKode Reset",
|
||||
Content: htmlBody,
|
||||
}
|
||||
n.EmailQueue.AddEmail(email)
|
||||
return nil
|
||||
}
|
||||
111
internal/api/nkode_api_test.go
Normal file
111
internal/api/nkode_api_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go-nkode/internal/db"
|
||||
"go-nkode/internal/email"
|
||||
"go-nkode/internal/models"
|
||||
"go-nkode/internal/security"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNKodeAPI(t *testing.T) {
|
||||
//db1 := NewInMemoryDb()
|
||||
//testNKodeAPI(t, &db1)
|
||||
|
||||
dbFile := os.Getenv("TEST_DB")
|
||||
|
||||
db2 := db.NewSqliteDB(dbFile)
|
||||
defer db2.CloseDb()
|
||||
testNKodeAPI(t, db2)
|
||||
|
||||
//if _, err := os.Stat(dbFile); err == nil {
|
||||
// err = os.Remove(dbFile)
|
||||
// assert.NoError(t, err)
|
||||
//} else {
|
||||
// assert.NoError(t, err)
|
||||
//}
|
||||
}
|
||||
|
||||
func testNKodeAPI(t *testing.T, db DbAccessor) {
|
||||
bufferSize := 100
|
||||
emailsPerSec := 14
|
||||
testClient := email.TestEmailClient{}
|
||||
queue := email.NewEmailQueue(bufferSize, emailsPerSec, &testClient)
|
||||
queue.Start()
|
||||
defer queue.Stop()
|
||||
attrsPerKey := 5
|
||||
numbOfKeys := 4
|
||||
for idx := 0; idx < 1; idx++ {
|
||||
userEmail := models.UserEmail("test_username" + security.GenerateRandomString(12) + "@example.com")
|
||||
passcodeLen := 4
|
||||
nkodePolicy := models.NewDefaultNKodePolicy()
|
||||
keypadSize := models.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
|
||||
nkodeApi := NewNKodeAPI(db, queue)
|
||||
customerId, err := nkodeApi.CreateNewCustomer(nkodePolicy, nil)
|
||||
assert.NoError(t, err)
|
||||
signupResponse, err := nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, false)
|
||||
assert.NoError(t, err)
|
||||
setInterface := signupResponse.UserIdxInterface
|
||||
sessionIdStr := signupResponse.SessionId
|
||||
sessionId, err := models.SessionIdFromString(sessionIdStr)
|
||||
assert.NoError(t, err)
|
||||
keypadSize = models.KeypadDimension{AttrsPerKey: numbOfKeys, NumbOfKeys: numbOfKeys}
|
||||
userPasscode := setInterface[:passcodeLen]
|
||||
setKeySelect, err := models.SelectKeyByAttrIdx(setInterface, userPasscode, keypadSize)
|
||||
assert.NoError(t, err)
|
||||
confirmInterface, err := nkodeApi.SetNKode(*customerId, sessionId, setKeySelect)
|
||||
assert.NoError(t, err)
|
||||
confirmKeySelect, err := models.SelectKeyByAttrIdx(confirmInterface, userPasscode, keypadSize)
|
||||
err = nkodeApi.ConfirmNKode(*customerId, sessionId, confirmKeySelect)
|
||||
assert.NoError(t, err)
|
||||
|
||||
keypadSize = models.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
|
||||
loginInterface, err := nkodeApi.GetLoginInterface(userEmail, *customerId)
|
||||
assert.NoError(t, err)
|
||||
loginKeySelection, err := models.SelectKeyByAttrIdx(loginInterface.UserIdxInterface, userPasscode, keypadSize)
|
||||
assert.NoError(t, err)
|
||||
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = nkodeApi.RenewAttributes(*customerId)
|
||||
assert.NoError(t, err)
|
||||
|
||||
loginInterface, err = nkodeApi.GetLoginInterface(userEmail, *customerId)
|
||||
assert.NoError(t, err)
|
||||
loginKeySelection, err = models.SelectKeyByAttrIdx(loginInterface.UserIdxInterface, userPasscode, keypadSize)
|
||||
assert.NoError(t, err)
|
||||
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
|
||||
assert.NoError(t, err)
|
||||
|
||||
/// Reset nKode
|
||||
attrsPerKey = 6
|
||||
keypadSize = models.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
|
||||
resetResponse, err := nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, true)
|
||||
assert.NoError(t, err)
|
||||
setInterface = resetResponse.UserIdxInterface
|
||||
sessionIdStr = resetResponse.SessionId
|
||||
sessionId, err = models.SessionIdFromString(sessionIdStr)
|
||||
assert.NoError(t, err)
|
||||
keypadSize = models.KeypadDimension{AttrsPerKey: numbOfKeys, NumbOfKeys: numbOfKeys}
|
||||
userPasscode = setInterface[:passcodeLen]
|
||||
setKeySelect, err = models.SelectKeyByAttrIdx(setInterface, userPasscode, keypadSize)
|
||||
assert.NoError(t, err)
|
||||
confirmInterface, err = nkodeApi.SetNKode(*customerId, sessionId, setKeySelect)
|
||||
assert.NoError(t, err)
|
||||
confirmKeySelect, err = models.SelectKeyByAttrIdx(confirmInterface, userPasscode, keypadSize)
|
||||
err = nkodeApi.ConfirmNKode(*customerId, sessionId, confirmKeySelect)
|
||||
assert.NoError(t, err)
|
||||
|
||||
keypadSize = models.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
|
||||
loginInterface2, err := nkodeApi.GetLoginInterface(userEmail, *customerId)
|
||||
assert.NoError(t, err)
|
||||
loginKeySelection, err = models.SelectKeyByAttrIdx(loginInterface2.UserIdxInterface, userPasscode, keypadSize)
|
||||
assert.NoError(t, err)
|
||||
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
|
||||
assert.NoError(t, err)
|
||||
signupResponse, err = nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, false)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
138
internal/db/in_memory_db.go
Normal file
138
internal/db/in_memory_db.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-nkode/internal/models"
|
||||
)
|
||||
|
||||
type InMemoryDb struct {
|
||||
Customers map[models.CustomerId]models.Customer
|
||||
Users map[models.UserId]models.User
|
||||
userIdMap map[string]models.UserId
|
||||
}
|
||||
|
||||
func NewInMemoryDb() InMemoryDb {
|
||||
return InMemoryDb{
|
||||
Customers: make(map[models.CustomerId]models.Customer),
|
||||
Users: make(map[models.UserId]models.User),
|
||||
userIdMap: make(map[string]models.UserId),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) GetCustomer(id models.CustomerId) (*models.Customer, error) {
|
||||
customer, exists := db.Customers[id]
|
||||
if !exists {
|
||||
return nil, errors.New(fmt.Sprintf("customer %s dne", customer.Id))
|
||||
}
|
||||
return &customer, nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) GetUser(username models.UserEmail, customerId models.CustomerId) (*models.User, error) {
|
||||
key := userIdKey(customerId, username)
|
||||
userId, exists := db.userIdMap[key]
|
||||
if !exists {
|
||||
return nil, errors.New(fmt.Sprintf("customer %s with username %s dne", customerId, username))
|
||||
}
|
||||
user, exists := db.Users[userId]
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("userId %s with customerId %s and username %s with no user", userId, customerId, username))
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) WriteNewCustomer(customer models.Customer) error {
|
||||
_, exists := db.Customers[customer.Id]
|
||||
|
||||
if exists {
|
||||
return errors.New(fmt.Sprintf("can write customer %s; already exists", customer.Id))
|
||||
}
|
||||
db.Customers[customer.Id] = customer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) WriteNewUser(user models.User) error {
|
||||
_, exists := db.Customers[user.CustomerId]
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("can't add user %s to customer %s: customer dne", user.Email, user.CustomerId))
|
||||
}
|
||||
userExists, _ := db.GetUser(user.Email, user.CustomerId)
|
||||
|
||||
if userExists != nil {
|
||||
return errors.New(fmt.Sprintf("can't write new user %s, alread exists", user.Email))
|
||||
}
|
||||
key := userIdKey(user.CustomerId, user.Email)
|
||||
db.userIdMap[key] = user.Id
|
||||
db.Users[user.Id] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) UpdateUserNKode(user models.User) error {
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) UpdateUserInterface(userId models.UserId, ui models.UserInterface) error {
|
||||
user, exists := db.Users[userId]
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("can't update user %s, dne", user.Id))
|
||||
}
|
||||
user.Interface = ui
|
||||
db.Users[userId] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) UpdateUserRefreshToken(userId models.UserId, refreshToken string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) Renew(id models.CustomerId) error {
|
||||
customer, exists := db.Customers[id]
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("customer %s does not exist", id))
|
||||
}
|
||||
setXor, attrsXor, err := customer.RenewKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Customers[id] = customer
|
||||
for _, user := range db.Users {
|
||||
if user.CustomerId == id {
|
||||
err = user.RenewKeys(setXor, attrsXor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Users[user.Id] = user
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) RefreshUserPasscode(user models.User, passocode []int, customerAttr models.CustomerAttributes) error {
|
||||
err := user.RefreshPasscode(passocode, customerAttr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Users[user.Id] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) RandomSvgInterface(kp models.KeypadDimension) ([]string, error) {
|
||||
return make([]string, kp.TotalAttrs()), nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) RandomSvgIdxInterface(kp models.KeypadDimension) (models.SvgIdInterface, error) {
|
||||
svgs := make(models.SvgIdInterface, kp.TotalAttrs())
|
||||
for idx := range svgs {
|
||||
svgs[idx] = idx
|
||||
}
|
||||
return svgs, nil
|
||||
}
|
||||
|
||||
func (db *InMemoryDb) GetSvgStringInterface(idxs models.SvgIdInterface) ([]string, error) {
|
||||
return make([]string, len(idxs)), nil
|
||||
}
|
||||
|
||||
func userIdKey(customerId models.CustomerId, username models.UserEmail) string {
|
||||
key := fmt.Sprintf("%s:%s", customerId, username)
|
||||
return key
|
||||
}
|
||||
578
internal/db/sqlite-init/json/academicons.json
Normal file
578
internal/db/sqlite-init/json/academicons.json
Normal file
File diff suppressed because one or more lines are too long
1431
internal/db/sqlite-init/json/akar-icons.json
Normal file
1431
internal/db/sqlite-init/json/akar-icons.json
Normal file
File diff suppressed because one or more lines are too long
5650
internal/db/sqlite-init/json/ant-design.json
Normal file
5650
internal/db/sqlite-init/json/ant-design.json
Normal file
File diff suppressed because one or more lines are too long
32209
internal/db/sqlite-init/json/arcticons.json
Normal file
32209
internal/db/sqlite-init/json/arcticons.json
Normal file
File diff suppressed because one or more lines are too long
2045
internal/db/sqlite-init/json/basil.json
Normal file
2045
internal/db/sqlite-init/json/basil.json
Normal file
File diff suppressed because it is too large
Load Diff
787
internal/db/sqlite-init/json/bitcoin-icons.json
Normal file
787
internal/db/sqlite-init/json/bitcoin-icons.json
Normal file
@@ -0,0 +1,787 @@
|
||||
{
|
||||
"prefix": "bitcoin-icons",
|
||||
"info": {
|
||||
"name": "Bitcoin Icons",
|
||||
"total": 250,
|
||||
"version": "0.1.10",
|
||||
"author": {
|
||||
"name": "Bitcoin Design Community",
|
||||
"url": "https://github.com/BitcoinDesign/Bitcoin-Icons"
|
||||
},
|
||||
"license": {
|
||||
"title": "MIT",
|
||||
"spdx": "MIT",
|
||||
"url": "https://github.com/BitcoinDesign/Bitcoin-Icons/blob/main/LICENSE-MIT"
|
||||
},
|
||||
"samples": [
|
||||
"exchange-outline",
|
||||
"brush-filled",
|
||||
"satoshi-v3-outline",
|
||||
"unlock-filled",
|
||||
"magic-wand-outline",
|
||||
"usb-outline"
|
||||
],
|
||||
"height": 24,
|
||||
"category": "General",
|
||||
"palette": false
|
||||
},
|
||||
"lastModified": 1722792992,
|
||||
"icons": {
|
||||
"address-book-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M7.005 19.996c.05.563.524 1.004 1.1 1.004h9.79c.61 0 1.105-.495 1.105-1.105V6.105c0-.576-.442-1.05-1.005-1.1q.005.05.005.1v13.79c0 .61-.495 1.105-1.105 1.105h-9.79a1 1 0 0 1-.1-.005\"/><path d=\"M5 4.105C5 3.495 5.495 3 6.105 3h9.79C16.505 3 17 3.495 17 4.105v13.79c0 .61-.495 1.105-1.105 1.105h-9.79C5.495 19 5 18.505 5 17.895zm3.34 10.502a.808.808 0 0 1 0-1.291a4.36 4.36 0 0 1 2.66-.9c1 0 1.923.335 2.66.9c.43.329.43.962 0 1.291c-.737.565-1.66.9-2.66.9s-1.923-.335-2.66-.9m2.658-2.965c1.136 0 2.058-1.153 2.058-2.575s-.922-2.574-2.058-2.574c-1.137 0-2.058 1.152-2.058 2.574s.921 2.575 2.058 2.575\"/></g>"
|
||||
},
|
||||
"address-book-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"M8 20.5h9.5a1 1 0 0 0 1-1V6\"/><path d=\"M5 4.075A1.07 1.07 0 0 1 6.066 3h9.444a1.07 1.07 0 0 1 1.066 1.075v13.41a1.07 1.07 0 0 1-1.066 1.075H6.066A1.07 1.07 0 0 1 5 17.485zm3.222 10.213a.79.79 0 0 1 0-1.256a4.2 4.2 0 0 1 2.566-.875c.965 0 1.855.326 2.566.875a.79.79 0 0 1 0 1.256a4.2 4.2 0 0 1-2.566.876a4.2 4.2 0 0 1-2.566-.876Zm2.564-2.884c1.096 0 1.985-1.12 1.985-2.504c0-1.382-.889-2.503-1.985-2.503S8.8 7.517 8.8 8.9s.888 2.504 1.985 2.504Z\" clip-rule=\"evenodd\"/></g>"
|
||||
},
|
||||
"alert-circle-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-7.75 4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0M13 6.5h-2v7h2z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"alert-circle-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path stroke-linecap=\"round\" d=\"M12 7v7m0 3.5v-1\"/></g>"
|
||||
},
|
||||
"alert-filled": {
|
||||
"body": "<circle cx=\"12\" cy=\"16.75\" r=\"1.25\" fill=\"currentColor\"/><path fill=\"currentColor\" d=\"M11 6h2v8h-2z\"/>"
|
||||
},
|
||||
"alert-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M12 7v7m0 3.5v-1\"/>"
|
||||
},
|
||||
"arrow-down-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12.013 2.25a.75.75 0 0 1 .75.75l-.012 16.19l5.72-5.708a.75.75 0 1 1 1.059 1.061l-7 6.988a.75.75 0 0 1-1.06 0l-7-6.988a.75.75 0 0 1 1.06-1.061l5.721 5.71L11.262 3a.75.75 0 0 1 .751-.75\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"arrow-down-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.013 3L12 20.789m7-6.776L12 21l-7-6.988\"/>"
|
||||
},
|
||||
"arrow-left-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M2.461 12a.75.75 0 0 1 .75-.75l17.79.012a.75.75 0 1 1-.002 1.5L3.21 12.75a.75.75 0 0 1-.749-.75\"/><path d=\"M10.517 4.47a.75.75 0 0 1 .001 1.06L4.06 12l6.458 6.47a.75.75 0 0 1-1.061 1.06l-6.988-7a.75.75 0 0 1 0-1.06l6.988-7a.75.75 0 0 1 1.06 0\"/></g>"
|
||||
},
|
||||
"arrow-left-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 12.013L3.211 12m6.777 7L3 12l6.988-7\"/>"
|
||||
},
|
||||
"arrow-right-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13.483 4.47a.75.75 0 0 1 1.06 0l6.988 7a.75.75 0 0 1 0 1.06l-6.988 7a.75.75 0 0 1-1.061-1.06l5.709-5.719L3 12.762a.75.75 0 0 1-.002-1.5l16.194-.01l-5.711-5.722a.75.75 0 0 1 0-1.06\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"arrow-right-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 12.013L20.789 12m-6.776 7L21 12l-6.988-7\"/>"
|
||||
},
|
||||
"arrow-up-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M11.47 2.47a.75.75 0 0 1 1.06 0l7 6.987a.75.75 0 1 1-1.06 1.061L12.751 4.81L12.762 21a.75.75 0 0 1-1.5.002l-.01-16.194l-5.722 5.711a.75.75 0 1 1-1.06-1.061z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"arrow-up-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.013 21L12 3.211m7 6.777L12 3L5 9.988\"/>"
|
||||
},
|
||||
"bell-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M10.1 5.374a2.001 2.001 0 0 1 3.8 0A5 5 0 0 1 17 10v4l2.146 2.146a.5.5 0 0 1-.353.854H5.207a.5.5 0 0 1-.353-.854L7 14v-4a5 5 0 0 1 3.1-4.626M10 18a2 2 0 1 0 4 0z\"/>"
|
||||
},
|
||||
"bell-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.479 5.749A4.5 4.5 0 0 1 16.5 10v3.7l2.073 1.935a.5.5 0 0 1-.341.865H5.769a.5.5 0 0 1-.342-.866L7.5 13.7V10a4.5 4.5 0 0 1 3.021-4.251a1.5 1.5 0 0 1 2.958 0\"/><path d=\"M10.585 18.5a1.5 1.5 0 0 0 2.83 0z\"/></g>"
|
||||
},
|
||||
"bitcoin-circle-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M10.864 14.36c.806.213 2.567.678 2.847-.447c.287-1.151-1.422-1.534-2.255-1.721q-.14-.031-.243-.056l-.542 2.174q.083.02.193.05m.759-3.177c.672.18 2.138.57 2.393-.452c.26-1.046-1.164-1.36-1.86-1.515l-.203-.047l-.492 1.972q.07.017.162.042\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M9.822 20.73a9 9 0 0 0 4.354-17.46A9 9 0 0 0 3.27 9.823A8.997 8.997 0 0 0 9.822 20.73m4.165-12.252c1.247.43 2.16 1.073 1.98 2.27c-.13.877-.616 1.302-1.261 1.45c.886.462 1.337 1.17.907 2.396c-.532 1.522-1.799 1.65-3.483 1.332l-.409 1.638l-.987-.246l.403-1.616a39 39 0 0 1-.787-.204l-.405 1.623l-.986-.246l.408-1.64l-.256-.067l-.448-.115l-1.285-.32l.49-1.13s.728.193.718.178c.28.07.404-.113.453-.234l.646-2.589l.084.021l.02.005a1 1 0 0 0-.103-.033l.46-1.848c.013-.21-.06-.475-.46-.574c.016-.01-.716-.179-.716-.179l.262-1.054l1.362.34v.005q.307.075.63.148l.405-1.622l.986.246l-.396 1.59c.265.06.532.121.791.186l.394-1.58l.988.247z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"bitcoin-circle-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M20.247 14.052a8.5 8.5 0 0 1-10.302 6.194C5.394 19.11 2.62 14.5 3.754 9.95s5.74-7.33 10.288-6.195c4.562 1.12 7.337 5.744 6.205 10.298Z\"/><path stroke-linecap=\"square\" stroke-linejoin=\"round\" d=\"m9.4 14.912l1.693-6.792m-1.456-.363L13.818 8.8c2.728.68 2.12 3.877-.786 3.153c3.184.794 2.86 4.578-.907 3.639c-1.841-.46-3.813-.95-3.813-.95m1.994-3.368l2.669.665m-1.397-3.698l.363-1.455m-2.42 9.703l.363-1.456m3.634-6.308l.363-1.455m-2.419 9.703l.363-1.456\"/></g>"
|
||||
},
|
||||
"bitcoin-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13.425 6.432c1.983.19 3.538.778 3.71 2.528c.117 1.276-.438 2.035-1.355 2.463c1.481.359 2.382 1.202 2.196 3.072c-.227 2.343-2.035 2.952-4.62 3.08l.004 2.42l-1.522.002l-.004-2.42q-.25-.002-.519.003c-.238.003-.484.006-.731-.001l.004 2.42l-1.52.001l-.004-2.42l-3.044-.058l.256-1.768s1.15.024 1.129.012c.423-.002.549-.293.58-.485l-.008-3.878l.012-2.76c-.046-.288-.248-.634-.87-.644c.033-.03-1.115.001-1.115.001L6 6.38l3.12-.005l-.004-2.37l1.571-.002l.004 2.37c.304-.008.603-.005.906-.003l.3.002l-.005-2.37L13.422 4zm-2.92 4.46l.076.002c.926.04 3.67.155 3.673-1.457c-.004-1.532-2.339-1.482-3.423-1.46q-.195.006-.327.005zm.129 4.75l-.134-.005v-2.91q.146.002.359-.002c1.282-.015 4.145-.05 4.132 1.494c.014 1.597-3.218 1.468-4.357 1.423\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"bitcoin-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M10.521 7.95v2.927c1.21.076 3.737.067 3.737-1.43S12.132 7.95 10.521 7.95Z\"/><path d=\"M13.425 6.432c1.983.19 3.538.778 3.71 2.528c.117 1.276-.438 2.035-1.355 2.463c1.481.359 2.382 1.202 2.196 3.072c-.227 2.343-2.035 2.952-4.62 3.08l.004 2.42l-1.522.002l-.004-2.42c-.389-.005-.819.015-1.25.002l.004 2.42l-1.52.001l-.004-2.42l-3.044-.058l.256-1.768s.724.012 1.129.012c.423-.002.549-.293.58-.485l-.008-3.878l.012-2.76c-.046-.288-.248-.634-.87-.644c.033-.03-1.115.001-1.115.001L6 6.38l3.12-.005l-.004-2.37l1.571-.002l.004 2.37c.403-.01.8-.003 1.205-.001l-.004-2.37L13.422 4z\"/><path d=\"M10.5 15.637c.991.036 4.506.247 4.49-1.418c.016-1.713-3.512-1.483-4.49-1.491z\"/></g>"
|
||||
},
|
||||
"block-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 8.344a.73.73 0 0 0-.364-.645l-7.494-4.395a2.21 2.21 0 0 0-2.23-.003L3.368 7.698a.73.73 0 0 0-.365.646H3v7.43h.004a.73.73 0 0 0 .376.622l7.563 4.242a2.21 2.21 0 0 0 2.167-.003l7.515-4.24a.73.73 0 0 0 .375-.62zm-16.236.564a.375.375 0 0 0-.368.653l6.359 3.588a2.59 2.59 0 0 0 2.551-.006l6.278-3.583a.375.375 0 1 0-.372-.651l-6.278 3.582c-.56.32-1.248.322-1.81.005z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"block-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M20.54 8.676v6.876a.69.69 0 0 1-.355.644l-7.132 4.024a2.1 2.1 0 0 1-2.056.002L3.82 16.197a.69.69 0 0 1-.355-.66V8.694a.69.69 0 0 1 .345-.654l7.156-4.172a2.1 2.1 0 0 1 2.117.002l7.112 4.17a.69.69 0 0 1 .344.636Z\"/><path d=\"M3.82 9.253a.699.699 0 0 1-.01-1.213l7.156-4.172a2.1 2.1 0 0 1 2.117.002l7.112 4.17a.699.699 0 0 1-.01 1.212l-7.132 4.024a2.1 2.1 0 0 1-2.056.003z\"/></g>"
|
||||
},
|
||||
"brush-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M7.5 5H5v6h14V5h-2.5v3a.5.5 0 0 1-1 0V5H14v4.5a.5.5 0 0 1-1 0V5H8.5v2a.5.5 0 0 1-1 0zM5 13v-1h14v1a2 2 0 0 1-2 2h-3v3a2 2 0 1 1-4 0v-3H7a2 2 0 0 1-2-2\"/>"
|
||||
},
|
||||
"brush-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 5.5h-13v7a2 2 0 0 0 2 2h3V18a1.5 1.5 0 0 0 3 0v-3.5h3a2 2 0 0 0 2-2z\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 11.5h12M16 6v2m-2.5-2v3.5M8 6v1\"/></g>"
|
||||
},
|
||||
"buoy-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M6.315 5.022c-.473.386-.907.82-1.293 1.293L7.877 9.17A5 5 0 0 1 9.17 7.877zm9.808 9.808a5 5 0 0 1-1.293 1.293l2.855 2.855a9 9 0 0 0 1.293-1.293zm3.45 2.036l-2.952-2.952c.244-.59.379-1.236.379-1.914s-.135-1.324-.38-1.914l2.953-2.952A8.96 8.96 0 0 1 21 12a8.96 8.96 0 0 1-1.427 4.866M10.086 7.38L7.134 4.426A8.96 8.96 0 0 1 12 3a8.96 8.96 0 0 1 4.866 1.427L13.914 7.38A5 5 0 0 0 12 7c-.678 0-1.324.135-1.914.38M5.022 17.685c.386.473.82.907 1.293 1.293l2.855-2.855a5 5 0 0 1-1.293-1.293zM7 12c0-.678.135-1.324.38-1.914L4.426 7.134A8.96 8.96 0 0 0 3 12a8.96 8.96 0 0 0 1.427 4.866l2.952-2.952A5 5 0 0 1 7 12m9.866 7.573l-2.952-2.952A5 5 0 0 1 12 17a5 5 0 0 1-1.914-.38l-2.952 2.953A8.96 8.96 0 0 0 12 21a8.96 8.96 0 0 0 4.866-1.427M14.83 7.877a5 5 0 0 1 1.293 1.293l2.855-2.855a9 9 0 0 0-1.293-1.293z\"/>"
|
||||
},
|
||||
"buoy-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><path d=\"m19 7l-2.5 2.5M17 5l-2.5 2.5m-5 9L7 19m.5-4.5L5 17m4.5-9.5L7 5m.5 4.5L5 7m12 12l-2.5-2.5m4.5.5l-2.5-2.5\"/></g>"
|
||||
},
|
||||
"calendar-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M5 7.5A1.5 1.5 0 0 1 6.5 6h11A1.5 1.5 0 0 1 19 7.5V9H5z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M16 4.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5m-8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5M5 10h14v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 16.5zm10.5 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm-3.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5M7 13.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm8.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm-3.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5M7 15.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm5.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"calendar-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M5.5 8A1.5 1.5 0 0 1 7 6.5h10A1.5 1.5 0 0 1 18.5 8v.5h-13z\"/><path stroke-linecap=\"round\" d=\"M16 5v2M8 5v2\"/><path d=\"M5.5 10.5h13V16a1.5 1.5 0 0 1-1.5 1.5H7A1.5 1.5 0 0 1 5.5 16z\"/><path stroke-linecap=\"round\" d=\"M15.75 12.25h1m-3.833 0h1m-3.834 0h1M7.25 14h1m7.5 0h1m-3.833 0h1m-3.834 0h1M7.25 15.75h1m4.667 0h1m-3.834 0h1\"/></g>"
|
||||
},
|
||||
"car-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M18.92 5.01C18.72 4.42 18.16 4 17.5 4h-11c-.66 0-1.21.42-1.42 1.01L3 11v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8zM6.5 15c-.83 0-1.5-.67-1.5-1.5S5.67 12 6.5 12s1.5.67 1.5 1.5S7.33 15 6.5 15m11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5M5 10l1.5-4.5h11L19 10z\"/>"
|
||||
},
|
||||
"car-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 5.51c-.2-.59-.59-1.01-1.25-1.01H6.75c-.66 0-1.04.42-1.25 1.01l-2 5.74v7.5c0 .55.2.75.75.75h.5c.55 0 .75-.2.75-.75V17.5h13v1.25c0 .55.2.75.75.75h.5c.55 0 .75-.2.75-.75v-7.5z\"/><path d=\"M6.5 14.5a1 1 0 1 1 0-2a1 1 0 1 1 0 2Zm11 0a1 1 0 1 1 0-2a1 1 0 1 1 0 2Z\"/><path stroke-linejoin=\"round\" d=\"M5.75 9.5L7 6h10l1.25 3.5z\"/></g>"
|
||||
},
|
||||
"caret-down-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M4.47 9.4a.75.75 0 0 1 1.06 0l6.364 6.364a.25.25 0 0 0 .354 0L18.612 9.4a.75.75 0 0 1 1.06 1.06l-6.364 6.364a1.75 1.75 0 0 1-2.475 0L4.47 10.46a.75.75 0 0 1 0-1.06\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"caret-down-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m19.142 9.929l-6.364 6.364a1 1 0 0 1-1.415 0L5 9.929\"/>"
|
||||
},
|
||||
"caret-left-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M14.601 4.47a.75.75 0 0 1 0 1.06l-6.364 6.364a.25.25 0 0 0 0 .354l6.364 6.364a.75.75 0 0 1-1.06 1.06L7.177 13.31a1.75 1.75 0 0 1 0-2.475L13.54 4.47a.75.75 0 0 1 1.06 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"caret-left-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m14.071 5l-6.364 6.364a1 1 0 0 0 0 1.414l6.364 6.364\"/>"
|
||||
},
|
||||
"caret-right-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M9.399 4.328a.75.75 0 0 1 1.06 0l6.364 6.363a1.75 1.75 0 0 1 0 2.475L10.46 19.53a.75.75 0 0 1-1.06-1.06l6.364-6.364a.25.25 0 0 0 0-.354L9.399 5.388a.75.75 0 0 1 0-1.06\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"caret-right-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m9.929 4.858l6.364 6.364a1 1 0 0 1 0 1.414L9.929 19\"/>"
|
||||
},
|
||||
"caret-up-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12.248 8.237a.25.25 0 0 0-.354 0L5.53 14.601a.75.75 0 1 1-1.06-1.06l6.363-6.364a1.75 1.75 0 0 1 2.475 0l6.364 6.364a.75.75 0 0 1-1.06 1.06z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"caret-up-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m5 14.071l6.364-6.364a1 1 0 0 1 1.414 0l6.364 6.364\"/>"
|
||||
},
|
||||
"cart-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M9.5 19.5a1 1 0 1 0 0-2a1 1 0 0 0 0 2m0 1a2 2 0 1 0 0-4a2 2 0 0 0 0 4m7-1a1 1 0 1 0 0-2a1 1 0 0 0 0 2m0 1a2 2 0 1 0 0-4a2 2 0 0 0 0 4M3 4a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .476.348L9.37 14.5H17a.5.5 0 0 1 0 1H9.004a.5.5 0 0 1-.476-.348L5.135 4.5H3.5A.5.5 0 0 1 3 4\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M8.5 13L6 6h13.337a.5.5 0 0 1 .48.637l-1.713 6a.5.5 0 0 1-.481.363z\"/>"
|
||||
},
|
||||
"cart-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"10\" cy=\"19\" r=\"1.5\"/><circle cx=\"17\" cy=\"19\" r=\"1.5\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.5 4h2l3.504 11H17\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.224 12.5L6.3 6.5h12.507a.5.5 0 0 1 .475.658l-1.667 5a.5.5 0 0 1-.474.342z\"/></g>"
|
||||
},
|
||||
"channel-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3 12a3.5 3.5 0 0 1 3.5-3.5h11a3.5 3.5 0 1 1 0 7h-11A3.5 3.5 0 0 1 3 12m3.5-2.5a2.5 2.5 0 0 0 0 5h11a2.5 2.5 0 0 0 0-5z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M5 12a1.5 1.5 0 0 1 1.5-1.5h7v3h-7A1.5 1.5 0 0 1 5 12m9.5-1.5h3a1.5 1.5 0 0 1 0 3h-3z\"/>"
|
||||
},
|
||||
"channel-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"17\" height=\"6\" x=\"3.5\" y=\"9\" rx=\"3\"/><path d=\"M5.5 12a1 1 0 0 1 1-1H13v2H6.5a1 1 0 0 1-1-1Zm9.5-1h2.5a1 1 0 1 1 0 2H15z\"/></g>"
|
||||
},
|
||||
"channels-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M3 8.5A1.5 1.5 0 0 1 4.5 7H13v3H4.5A1.5 1.5 0 0 1 3 8.5M14 7h5.5a1.5 1.5 0 0 1 0 3H14zM3 12.5A1.5 1.5 0 0 1 4.5 11H10v3H4.5A1.5 1.5 0 0 1 3 12.5m8-1.5h8.5a1.5 1.5 0 0 1 0 3H11zm-8 5.5A1.5 1.5 0 0 1 4.5 15H16v3H4.5A1.5 1.5 0 0 1 3 16.5M17 15h2.5a1.5 1.5 0 0 1 0 3H17z\"/>"
|
||||
},
|
||||
"channels-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M3.5 8.5a1 1 0 0 1 1-1h8v2h-8a1 1 0 0 1-1-1Zm11-1h5a1 1 0 1 1 0 2h-5zm-11 5a1 1 0 0 1 1-1h5v2h-5a1 1 0 0 1-1-1Zm8-1h8a1 1 0 1 1 0 2h-8zm-8 5a1 1 0 0 1 1-1h11v2h-11a1 1 0 0 1-1-1Zm14-1h2a1 1 0 1 1 0 2h-2z\"/>"
|
||||
},
|
||||
"check-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.381 5.354a.75.75 0 0 1 .265 1.027l-7.087 12a.75.75 0 0 1-1.164.16L5.48 13.838a.75.75 0 0 1 1.038-1.084l4.23 4.051L17.353 5.62a.75.75 0 0 1 1.027-.265\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"check-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 13.295L10.913 18L18 6\"/>"
|
||||
},
|
||||
"clear-character-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 5a.53.53 0 0 0-.431.222l-3.923 5.486a1.64 1.64 0 0 0-.047 1.839L7.58 18.77A.5.5 0 0 0 8 19h11.359c.906 0 1.641-.735 1.641-1.641V6.64C21 5.735 20.265 5 19.359 5zm1.646 3.146a.5.5 0 0 1 .708 0l3.127 3.128l3.094-3.126a.5.5 0 0 1 .71.704l-3.097 3.129l3.166 3.165a.5.5 0 0 1-.708.708l-3.162-3.163l-3.129 3.16a.5.5 0 0 1-.71-.703l3.132-3.164l-3.13-3.13a.5.5 0 0 1 0-.708\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"clear-character-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m7.5 5.5l-3.447 5.29a1.64 1.64 0 0 0-.043 1.723L7.5 18.5h11.36a1.64 1.64 0 0 0 1.64-1.641V7.14a1.64 1.64 0 0 0-1.64-1.641zm2.5 3l7 7m-7 0l6.93-7\"/>"
|
||||
},
|
||||
"clock-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-8.5-.207V6.97a.5.5 0 1 0-1 0v5.015a.5.5 0 0 0 .146.369l2.829 2.828a.5.5 0 1 0 .707-.707z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"clock-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 7v5l2.8 2.8\"/></g>"
|
||||
},
|
||||
"cloud-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M9.5 6a4.5 4.5 0 0 1 4.366 3.404a3.5 3.5 0 0 1 5.105 2.64A2.5 2.5 0 0 1 18.5 17h-12a3.5 3.5 0 0 1-1.497-6.665A4.5 4.5 0 0 1 9.5 6\"/>"
|
||||
},
|
||||
"cloud-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M9.5 6.5a4 4 0 0 1 3.993 3.77A3 3 0 0 1 18.5 12.5a2 2 0 1 1 0 4h-12a3 3 0 0 1-.996-5.83L5.5 10.5a4 4 0 0 1 4-4Z\"/>"
|
||||
},
|
||||
"code-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13.737 6.789a.75.75 0 0 1 .475.948l-3 9a.75.75 0 0 1-1.423-.474l3-9a.75.75 0 0 1 .948-.474m2.275 1.141a.75.75 0 0 1 1.058.082l3 3.5a.75.75 0 0 1 0 .976l-3 3.5a.75.75 0 0 1-1.14-.976L18.513 12l-2.581-3.012a.75.75 0 0 1 .08-1.057M7.988 7.93a.75.75 0 0 1 .081 1.058L5.488 12l2.581 3.012a.75.75 0 0 1-1.138.976l-3-3.5a.75.75 0 0 1 0-.976l3-3.5a.75.75 0 0 1 1.057-.081\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"code-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"m10.5 16.5l3-9\"/><path stroke-linejoin=\"round\" d=\"m16.5 8.5l3 3.5l-3 3.5m-9-7l-3 3.5l3 3.5\"/></g>"
|
||||
},
|
||||
"coins-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M10 16a6 6 0 0 0 5.97-6.597a5.001 5.001 0 1 1-6.567 6.568Q9.7 16 10 16\"/><circle cx=\"10\" cy=\"10\" r=\"5\" fill=\"currentColor\"/>"
|
||||
},
|
||||
"coins-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M16.495 10.255a6.5 6.5 0 0 1-6.24 6.24a4.5 4.5 0 1 0 6.24-6.24Z\"/><circle cx=\"10\" cy=\"10\" r=\"4.5\"/></g>"
|
||||
},
|
||||
"confirmations-0-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13.018 4.806a.5.5 0 0 1 .593-.385a7.75 7.75 0 0 1 4.148 2.395a.5.5 0 0 1-.743.669a6.75 6.75 0 0 0-3.612-2.086a.5.5 0 0 1-.386-.593m5.722 4.48a.5.5 0 0 1 .63.32a7.75 7.75 0 0 1 0 4.79a.5.5 0 0 1-.95-.308a6.75 6.75 0 0 0 0-4.172a.5.5 0 0 1 .32-.63m-1.017 7.195a.5.5 0 0 1 .037.706a7.75 7.75 0 0 1-4.149 2.395a.5.5 0 1 1-.208-.978a6.75 6.75 0 0 0 3.613-2.086a.5.5 0 0 1 .707-.037m-11.445 0a.5.5 0 0 1 .706.037a6.75 6.75 0 0 0 3.613 2.086a.5.5 0 1 1-.208.978a7.75 7.75 0 0 1-4.148-2.395a.5.5 0 0 1 .037-.706M5.26 9.286a.5.5 0 0 1 .32.63a6.75 6.75 0 0 0 0 4.172a.5.5 0 1 1-.95.309a7.75 7.75 0 0 1 0-4.79a.5.5 0 0 1 .63-.321m5.722-4.48a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-0-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M13.507 4.908a7.25 7.25 0 0 1 3.88 2.24m1.508 2.612a7.25 7.25 0 0 1 0 4.48m-1.507 2.611a7.25 7.25 0 0 1-3.88 2.24m-3.015.001a7.25 7.25 0 0 1-3.88-2.24M5.105 14.24a7.25 7.25 0 0 1 0-4.48m1.507-2.611a7.25 7.25 0 0 1 3.88-2.24\"/>"
|
||||
},
|
||||
"confirmations-1-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M13 5.66c0 .244.177.45.415.508a6 6 0 0 1 2.928 1.692a.53.53 0 0 0 .646.105l1.773-1.024a.477.477 0 0 0 .139-.719a9 9 0 0 0-5.348-3.087a.477.477 0 0 0-.553.48z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.74 9.286a.5.5 0 0 1 .63.32a7.75 7.75 0 0 1 0 4.79a.5.5 0 0 1-.95-.308a6.75 6.75 0 0 0 0-4.172a.5.5 0 0 1 .32-.63m-1.017 7.195a.5.5 0 0 1 .037.706a7.75 7.75 0 0 1-4.149 2.395a.5.5 0 1 1-.208-.978a6.75 6.75 0 0 0 3.613-2.086a.5.5 0 0 1 .707-.037m-11.445 0a.5.5 0 0 1 .706.037a6.75 6.75 0 0 0 3.613 2.086a.5.5 0 1 1-.208.978a7.75 7.75 0 0 1-4.148-2.395a.5.5 0 0 1 .037-.706M5.26 9.286a.5.5 0 0 1 .32.63a6.75 6.75 0 0 0 0 4.172a.5.5 0 1 1-.95.309a7.75 7.75 0 0 1 0-4.79a.5.5 0 0 1 .63-.321m5.722-4.48a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-1-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linejoin=\"round\" d=\"m16.281 7.796l2.215-1.278A8.5 8.5 0 0 0 13.5 3.633V6.19a6 6 0 0 1 2.781 1.607Z\"/><path stroke-linecap=\"round\" d=\"M18.895 9.76a7.25 7.25 0 0 1 0 4.48m-1.507 2.611a7.25 7.25 0 0 1-3.88 2.24m-3.015.001a7.25 7.25 0 0 1-3.88-2.24M5.105 14.24a7.25 7.25 0 0 1 0-4.48m1.507-2.611a7.25 7.25 0 0 1 3.88-2.24\"/></g>"
|
||||
},
|
||||
"confirmations-2-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M13 5.66c0 .244.177.45.415.508a6 6 0 0 1 2.928 1.692a.53.53 0 0 0 .646.105l1.773-1.024a.477.477 0 0 0 .139-.719a9 9 0 0 0-5.348-3.087a.477.477 0 0 0-.553.48zM18 12c0-.588-.084-1.155-.242-1.692a.53.53 0 0 1 .233-.612l1.77-1.023a.478.478 0 0 1 .693.24a9 9 0 0 1 0 6.174a.477.477 0 0 1-.692.24l-1.771-1.023a.53.53 0 0 1-.233-.612A6 6 0 0 0 18 12\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M17.723 16.481a.5.5 0 0 1 .037.706a7.75 7.75 0 0 1-4.149 2.395a.5.5 0 1 1-.208-.978a6.75 6.75 0 0 0 3.613-2.086a.5.5 0 0 1 .707-.037m-11.445 0a.5.5 0 0 1 .706.037a6.75 6.75 0 0 0 3.613 2.086a.5.5 0 1 1-.208.978a7.75 7.75 0 0 1-4.148-2.395a.5.5 0 0 1 .037-.706M5.26 9.286a.5.5 0 0 1 .32.63a6.75 6.75 0 0 0 0 4.172a.5.5 0 1 1-.95.309a7.75 7.75 0 0 1 0-4.79a.5.5 0 0 1 .63-.321m5.722-4.48a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-2-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linejoin=\"round\" d=\"M17.783 10.394a6.03 6.03 0 0 1-.001 3.213l2.214 1.277a8.5 8.5 0 0 0 0-5.768zm-1.502-2.598l2.215-1.278A8.5 8.5 0 0 0 13.5 3.633V6.19a6 6 0 0 1 2.781 1.607Z\"/><path stroke-linecap=\"round\" d=\"M17.388 16.851a7.25 7.25 0 0 1-3.88 2.24m-3.015.001a7.25 7.25 0 0 1-3.88-2.24M5.105 14.24a7.25 7.25 0 0 1 0-4.48m1.507-2.611a7.25 7.25 0 0 1 3.88-2.24\"/></g>"
|
||||
},
|
||||
"confirmations-3-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M13 20.385c0 .298.26.531.553.48a9 9 0 0 0 5.348-3.087a.477.477 0 0 0-.14-.72l-1.772-1.023a.53.53 0 0 0-.646.105a6 6 0 0 1-2.928 1.692a.53.53 0 0 0-.415.508zm4.758-6.693a.53.53 0 0 0 .233.612l2.102 1.214a.155.155 0 0 0 .222-.074a9 9 0 0 0 0-6.888a.155.155 0 0 0-.222-.074l-2.102 1.214a.53.53 0 0 0-.233.612c.158.537.242 1.104.242 1.692s-.084 1.155-.242 1.692m.606-8.056q.282.282.537.586a.478.478 0 0 1-.14.72L16.99 7.964a.53.53 0 0 1-.646-.105a6 6 0 0 0-2.928-1.692A.53.53 0 0 1 13 5.66V3.615c0-.298.26-.531.553-.48a9 9 0 0 1 4.811 2.501\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6.278 16.481a.5.5 0 0 1 .706.037a6.75 6.75 0 0 0 3.613 2.086a.5.5 0 1 1-.208.978a7.75 7.75 0 0 1-4.148-2.395a.5.5 0 0 1 .037-.706M5.26 9.286a.5.5 0 0 1 .32.63a6.75 6.75 0 0 0 0 4.172a.5.5 0 1 1-.95.309a7.75 7.75 0 0 1 0-4.79a.5.5 0 0 1 .63-.321m5.722-4.48a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-3-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linejoin=\"round\" d=\"M13.5 20.367a8.5 8.5 0 0 0 4.996-2.885l-2.215-1.278A6 6 0 0 1 13.5 17.81zm7-8.367c0 .993-.174 1.968-.504 2.884l-2.213-1.277a6.04 6.04 0 0 0-.001-3.214l2.214-1.277c.33.916.504 1.891.504 2.884Zm-2.004-5.482A8.5 8.5 0 0 0 13.5 3.633V6.19a6 6 0 0 1 2.781 1.607z\"/><path stroke-linecap=\"round\" d=\"M10.493 19.092a7.25 7.25 0 0 1-3.88-2.24M5.105 14.24a7.25 7.25 0 0 1 0-4.48m1.507-2.611a7.25 7.25 0 0 1 3.88-2.24\"/></g>"
|
||||
},
|
||||
"confirmations-4-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M11 18.34a.53.53 0 0 0-.415-.508a6 6 0 0 1-2.928-1.692a.53.53 0 0 0-.646-.105L5.238 17.06a.477.477 0 0 0-.139.719a9 9 0 0 0 5.348 3.087a.477.477 0 0 0 .553-.48zm2 2.045c0 .298.26.531.553.48a9 9 0 0 0 5.348-3.087a.477.477 0 0 0-.14-.72l-1.772-1.023a.53.53 0 0 0-.646.105a6 6 0 0 1-2.928 1.692a.53.53 0 0 0-.415.508zm4.758-6.693a.53.53 0 0 0 .233.612l1.77 1.023c.259.149.59.04.693-.24a9 9 0 0 0 0-6.174a.477.477 0 0 0-.692-.24L17.99 9.696a.53.53 0 0 0-.233.612c.158.537.242 1.104.242 1.692s-.084 1.155-.242 1.692M16.542 4.23A9 9 0 0 1 18.9 6.222a.478.478 0 0 1-.14.72l-1.77 1.022a.53.53 0 0 1-.646-.105a6 6 0 0 0-2.928-1.692A.53.53 0 0 1 13 5.66V3.615c0-.298.26-.531.553-.48a9 9 0 0 1 2.988 1.095\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M17.723 16.481a.5.5 0 0 1 .037.706a7.75 7.75 0 0 1-4.149 2.395a.5.5 0 1 1-.208-.978a6.75 6.75 0 0 0 3.613-2.086a.5.5 0 0 1 .707-.037m-11.445 0a.5.5 0 0 1 .706.037a6.75 6.75 0 0 0 3.613 2.086a.5.5 0 1 1-.208.978a7.75 7.75 0 0 1-4.148-2.395a.5.5 0 0 1 .037-.706M5.26 9.286a.5.5 0 0 1 .32.63a6.75 6.75 0 0 0 0 4.172a.5.5 0 1 1-.95.309a7.75 7.75 0 0 1 0-4.79a.5.5 0 0 1 .63-.321m5.722-4.48a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-4-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linejoin=\"round\" d=\"m7.719 16.204l-2.215 1.278a8.5 8.5 0 0 0 4.996 2.885V17.81a6 6 0 0 1-2.781-1.607Zm10.064-5.81l2.213-1.278a8.5 8.5 0 0 1 0 5.768l-2.213-1.277a6.04 6.04 0 0 0-.001-3.214Zm.713-3.876L16.28 7.796A6 6 0 0 0 13.5 6.19V3.633a8.5 8.5 0 0 1 4.996 2.885ZM13.5 20.367a8.5 8.5 0 0 0 4.996-2.885l-2.215-1.278A6 6 0 0 1 13.5 17.81z\"/><path stroke-linecap=\"round\" d=\"M5.105 14.24a7.25 7.25 0 0 1 0-4.48m1.507-2.611a7.25 7.25 0 0 1 3.88-2.24\"/></g>"
|
||||
},
|
||||
"confirmations-5-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M11 18.34a.53.53 0 0 0-.415-.508a6 6 0 0 1-2.928-1.692a.53.53 0 0 0-.646-.105L5.238 17.06a.477.477 0 0 0-.139.719a9 9 0 0 0 5.8 3.155a.09.09 0 0 0 .101-.09zM18 12c0-.588-.084-1.155-.242-1.692a.53.53 0 0 1 .233-.612l1.77-1.023c.26-.149.59-.04.693.24a9 9 0 0 1-.151 6.56a.13.13 0 0 1-.182.06l-2.13-1.229a.53.53 0 0 1-.233-.612A6 6 0 0 0 18 12M6.01 9.696a.53.53 0 0 1 .232.612A6 6 0 0 0 6 12c0 .588.084 1.155.242 1.692a.53.53 0 0 1-.233.612l-1.77 1.023a.477.477 0 0 1-.693-.24a9 9 0 0 1 0-6.174a.477.477 0 0 1 .692-.24zm10.98 6.339a.53.53 0 0 0-.647.105a6 6 0 0 1-2.928 1.692a.53.53 0 0 0-.415.508v2.045c0 .298.26.531.553.48a9 9 0 0 0 5.348-3.087a.477.477 0 0 0-.14-.72zm-.647-8.175a.53.53 0 0 0 .646.105l1.773-1.024a.477.477 0 0 0 .139-.719a9 9 0 0 0-5.348-3.087a.477.477 0 0 0-.553.48V5.66c0 .244.177.45.415.508a6 6 0 0 1 2.928 1.692\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M10.982 4.806a.5.5 0 0 1-.385.593a6.75 6.75 0 0 0-3.613 2.086a.5.5 0 1 1-.743-.67a7.75 7.75 0 0 1 4.148-2.394a.5.5 0 0 1 .593.385\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"confirmations-5-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linejoin=\"round\" d=\"M6.217 13.607A6 6 0 0 1 6 12c0-.556.076-1.095.217-1.607L4.004 9.117a8.5 8.5 0 0 0 0 5.768zm1.502 2.597l-2.215 1.278q.226.27.477.52a8.5 8.5 0 0 0 4.519 2.365V17.81a6 6 0 0 1-2.781-1.607Zm10.064-5.81l2.213-1.278a8.5 8.5 0 0 1 0 5.768l-2.213-1.277a6.04 6.04 0 0 0-.001-3.214Zm.713-3.876L16.28 7.796A6 6 0 0 0 13.5 6.19V3.633a8.5 8.5 0 0 1 4.996 2.885ZM13.5 17.811a6 6 0 0 0 2.781-1.607l2.215 1.278a8.5 8.5 0 0 1-4.996 2.885z\"/><path stroke-linecap=\"round\" d=\"M6.612 7.149a7.25 7.25 0 0 1 3.88-2.24\"/></g>"
|
||||
},
|
||||
"confirmations-6-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M4.236 8.672a.477.477 0 0 0-.692.24A9 9 0 0 0 3 12c0 1.085.192 2.126.544 3.089c.102.28.434.388.692.239l1.773-1.024a.53.53 0 0 0 .233-.612A6 6 0 0 1 6 12c0-.588.084-1.155.242-1.692a.53.53 0 0 0-.233-.612zm.864-2.45a.477.477 0 0 0 .138.72l1.773 1.023a.53.53 0 0 0 .646-.105a6 6 0 0 1 2.928-1.692A.53.53 0 0 0 11 5.66V3.613c0-.298-.26-.53-.553-.48A9 9 0 0 0 5.1 6.222m11.89 9.813a.53.53 0 0 0-.647.105a6 6 0 0 1-2.928 1.692a.53.53 0 0 0-.415.508v2.047c0 .298.26.53.553.48a9 9 0 0 0 5.348-3.089a.477.477 0 0 0-.139-.72zm3.466-.946a.477.477 0 0 1-.692.239l-1.773-1.024a.53.53 0 0 1-.233-.612A6 6 0 0 0 18 12c0-.588-.084-1.155-.242-1.692a.53.53 0 0 1 .233-.612l1.773-1.024a.477.477 0 0 1 .692.24c.352.962.544 2.003.544 3.088a9 9 0 0 1-.544 3.089M11 20.387c0 .298-.26.53-.553.48A9 9 0 0 1 5.1 17.778a.477.477 0 0 1 .139-.72l1.773-1.023a.53.53 0 0 1 .646.105a6 6 0 0 0 2.928 1.692a.53.53 0 0 1 .415.508zm2-16.774c0-.298.26-.53.553-.48A9 9 0 0 1 18.9 6.222c.191.229.12.57-.139.72l-1.773 1.023a.53.53 0 0 1-.646-.105a6 6 0 0 0-2.928-1.692A.53.53 0 0 1 13 5.66z\"/>"
|
||||
},
|
||||
"confirmations-6-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linejoin=\"round\" d=\"M10.5 3.632a8.5 8.5 0 0 0-4.996 2.886L7.72 7.796A6 6 0 0 1 10.5 6.19zM3.5 12a8.5 8.5 0 0 1 .502-2.885l2.215 1.278A6 6 0 0 0 6 12c0 .556.076 1.095.217 1.607l-2.215 1.278A8.5 8.5 0 0 1 3.5 12Zm2.004 5.482a8.5 8.5 0 0 0 4.996 2.886v-2.557a6 6 0 0 1-2.781-1.607zm12.279-7.088a6.03 6.03 0 0 1-.001 3.213l2.216 1.279A8.5 8.5 0 0 0 20.5 12a8.5 8.5 0 0 0-.502-2.885zm.713-3.876A8.5 8.5 0 0 0 13.5 3.632v2.557a6 6 0 0 1 2.781 1.607zM13.5 17.811a6 6 0 0 0 2.781-1.607l2.215 1.278a8.5 8.5 0 0 1-4.996 2.886z\"/>"
|
||||
},
|
||||
"console-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.22 4.47a.75.75 0 0 1 1.06 0l7 7a.75.75 0 0 1 0 1.06l-7 7a.75.75 0 0 1-1.06-1.06L9.69 12L3.22 5.53a.75.75 0 0 1 0-1.06M9.25 19a.75.75 0 0 1 .75-.75h10.25a.75.75 0 0 1 0 1.5H10a.75.75 0 0 1-.75-.75\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"console-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 19h10.5M3.5 5l7 7l-7 7\"/>"
|
||||
},
|
||||
"contacts-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M15.436 14.778c-.595-.25-1.336-.563-1.336-.803v-1.57A3.55 3.55 0 0 0 15.5 9.6V7.5C15.5 5.57 13.93 4 12 4S8.5 5.57 8.5 7.5v2.1a3.55 3.55 0 0 0 1.4 2.806v1.569c0 .226-.734.54-1.323.792C7.152 15.376 5 16.296 5 18.7v.35h14v-.35c0-2.42-2.144-3.324-3.564-3.922\" opacity=\".25\"/><path fill=\"currentColor\" d=\"M8.5 9.5v-2a3.5 3.5 0 1 1 7 0v2c0 1.19-.593 2.24-1.5 2.873v.95a1 1 0 0 0 .629.928l1.586.635A4.43 4.43 0 0 1 19 19H5a4.43 4.43 0 0 1 2.785-4.114l1.586-.635a1 1 0 0 0 .629-.928v-.95A3.5 3.5 0 0 1 8.5 9.5\"/>"
|
||||
},
|
||||
"contacts-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 9.5v-2a3 3 0 1 1 6 0v2a3 3 0 0 1-1.5 2.599v1.224a1 1 0 0 0 .629.928l2.05.82A3.69 3.69 0 0 1 18.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 0 0 .629-.929v-1.224A3 3 0 0 1 9 9.5\"/>"
|
||||
},
|
||||
"copy-filled": {
|
||||
"body": "<rect width=\"10\" height=\"14\" x=\"6\" y=\"6\" fill=\"currentColor\" rx=\"1.5\"/><path fill=\"currentColor\" d=\"M8.064 5.064A1.5 1.5 0 0 1 8.5 5h7A1.5 1.5 0 0 1 17 6.5v11a1.5 1.5 0 0 1-.064.436A1.5 1.5 0 0 0 18 16.5v-11A1.5 1.5 0 0 0 16.5 4h-7a1.5 1.5 0 0 0-1.436 1.064\"/>"
|
||||
},
|
||||
"copy-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"9\" height=\"13\" x=\"6.5\" y=\"6.5\" rx=\"1.5\"/><path d=\"M8.5 6A1.5 1.5 0 0 1 10 4.5h6A1.5 1.5 0 0 1 17.5 6v10a1.5 1.5 0 0 1-1.5 1.5\"/></g>"
|
||||
},
|
||||
"credit-card-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M4 7.5A1.5 1.5 0 0 1 5.5 6h13A1.5 1.5 0 0 1 20 7.5V9H4z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M4 10h16v6.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 16.5zm3 3a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 7 13m.5 1.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"credit-card-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"15\" height=\"11\" x=\"4.5\" y=\"6.5\" rx=\"1.5\"/><path d=\"M19.5 9.5h-15\"/><path stroke-linecap=\"round\" d=\"M11.5 12.5h-4m3 2h-3\"/></g>"
|
||||
},
|
||||
"cross-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M5.47 5.47a.75.75 0 0 1 1.06 0l12 12a.75.75 0 1 1-1.06 1.06l-12-12a.75.75 0 0 1 0-1.06\"/><path d=\"M18.53 5.47a.75.75 0 0 1 0 1.06l-12 12a.75.75 0 0 1-1.06-1.06l12-12a.75.75 0 0 1 1.06 0\"/></g>"
|
||||
},
|
||||
"cross-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m6 6l12 12m0-12L6 18\"/>"
|
||||
},
|
||||
"devices-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8.5 4A1.5 1.5 0 0 0 7 5.5v13A1.5 1.5 0 0 0 8.5 20h7a1.5 1.5 0 0 0 1.5-1.5v-13A1.5 1.5 0 0 0 15.5 4zm1.25 2a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75zM9 14.25c0 .414.336.75.75.75h.5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0-.75.75m4.75.75a.75.75 0 0 1 0-1.5h.5a.75.75 0 0 1 0 1.5z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"devices-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"9\" height=\"15\" x=\"7.5\" y=\"4.5\" rx=\"1.5\"/><rect width=\"5\" height=\"5\" x=\"9.5\" y=\"6.5\" rx=\".75\"/><rect width=\"1.5\" height=\"1\" rx=\".5\" transform=\"matrix(1 0 0 -1 9.5 14.5)\"/><rect width=\"1.5\" height=\"1\" rx=\".5\" transform=\"matrix(1 0 0 -1 13 14.5)\"/></g>"
|
||||
},
|
||||
"edit-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M3.995 17.207V19.5a.5.5 0 0 0 .5.5h2.298a.5.5 0 0 0 .353-.146l9.448-9.448l-3-3l-9.452 9.448a.5.5 0 0 0-.147.353m10.837-11.04l3 3l1.46-1.46a1 1 0 0 0 0-1.414l-1.585-1.586a1 1 0 0 0-1.414 0z\"/>"
|
||||
},
|
||||
"edit-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M4.5 17.207V19a.5.5 0 0 0 .5.5h1.793a.5.5 0 0 0 .353-.146l8.5-8.5l-2.5-2.5l-8.5 8.5a.5.5 0 0 0-.146.353ZM15.09 6.41l2.5 2.5l1.203-1.203a1 1 0 0 0 0-1.414l-1.086-1.086a1 1 0 0 0-1.414 0z\"/>"
|
||||
},
|
||||
"ellipsis-filled": {
|
||||
"body": "<circle cx=\"6.5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"/><circle cx=\"17.5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"/>"
|
||||
},
|
||||
"ellipsis-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"7\" cy=\"12\" r=\"1\"/><circle cx=\"12.5\" cy=\"12\" r=\"1\"/><circle cx=\"18\" cy=\"12\" r=\"1\"/></g>"
|
||||
},
|
||||
"exchange-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M19.013 10.15a.55.55 0 0 0-.145.493Q19 11.303 19 12a7 7 0 0 1-12.167 4.722a.53.53 0 0 0-.76-.032a.475.475 0 0 0-.027.654a8 8 0 0 0 13.788-6.971c-.079-.383-.545-.498-.821-.222m-1.224-2.99a.475.475 0 0 0 .018-.664A8 8 0 0 0 4.192 13.75c.084.378.546.489.82.215c.13-.13.18-.32.142-.5A7 7 0 0 1 17.038 7.14a.53.53 0 0 0 .75.021\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.125 5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 1 1 0-1h1.5V5.5a.5.5 0 0 1 .5-.5M5.521 16.147A.5.5 0 0 1 5.875 16h2a.5.5 0 0 1 0 1h-1.5v1.5a.5.5 0 1 1-1 0v-2a.5.5 0 0 1 .146-.354\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M12.224 12.334v1.941c.662-.074 1.138-.457 1.138-1.006c0-.558-.525-.766-1.116-.93zm-1.433-1.679c0 .555.57.785.985.902V9.741c-.602.08-.985.44-.985.914\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 17a5 5 0 1 0 0-10a5 5 0 0 0 0 10m.224-1.5h-.448v-.61c-1.075-.07-1.731-.672-1.794-1.534h.7c.05.563.52.859 1.094.919v-2.07l-.219-.063c-.875-.251-1.422-.7-1.422-1.454c0-.86.703-1.455 1.64-1.55V8.5h.45v.629c.99.066 1.69.673 1.716 1.471h-.656c-.06-.498-.481-.81-1.06-.867v1.944l.218.06c.58.154 1.575.493 1.575 1.543c0 .853-.645 1.531-1.794 1.61z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"exchange-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M17.757 7.193a7.5 7.5 0 0 0-13.108 6.303M19.3 10.274a7.5 7.5 0 0 1-13.186 6.375\"/><path stroke-linejoin=\"round\" d=\"M18.125 5.5v2h-2m-8.25 9h-2v2\"/><path d=\"M12 8v8\"/><path stroke-linejoin=\"round\" d=\"M13.81 10.152c-.12-.53-.803-1.12-1.804-1.12s-1.77.65-1.77 1.47c0 1.864 3.711.906 3.711 3.07c0 .781-.94 1.444-1.94 1.444s-1.694-.615-1.899-1.274\"/></g>"
|
||||
},
|
||||
"exit-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M15.99 7.823a.75.75 0 0 1 1.061.021l3.49 3.637a.75.75 0 0 1 0 1.038l-3.49 3.637a.75.75 0 0 1-1.082-1.039l2.271-2.367h-6.967a.75.75 0 0 1 0-1.5h6.968l-2.272-2.367a.75.75 0 0 1 .022-1.06\"/><path d=\"M3.25 4A.75.75 0 0 1 4 3.25h9.455a.75.75 0 0 1 .75.75v3a.75.75 0 1 1-1.5 0V4.75H4.75v14.5h7.954V17a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-.75.75H4a.75.75 0 0 1-.75-.75z\"/></g>"
|
||||
},
|
||||
"exit-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19.285 12h-8.012m5.237 3.636L20 12l-3.49-3.636M13.455 7V4H4v16h9.455v-3\"/>"
|
||||
},
|
||||
"export-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M11.37 10.384a1.5 1.5 0 0 0 0 2.121l.067.067a1.5 1.5 0 0 0 2.122 0l3.115-3.115c.144-.144.253-.31.326-.488V17a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h7.851a1.5 1.5 0 0 0-.366.269z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M19.218 4.782a.5.5 0 0 1 0 .708l-6.364 6.364a.5.5 0 0 1-.708-.707l6.364-6.365a.5.5 0 0 1 .707 0\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M14 4.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V5h-4.5a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"export-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M12 7.5H7A1.5 1.5 0 0 0 5.5 9v8A1.5 1.5 0 0 0 7 18.5h8a1.5 1.5 0 0 0 1.5-1.5v-5\"/><path stroke-linejoin=\"round\" d=\"m12.5 11.5l6.364-6.364M14.5 4.5h5v5\"/></g>"
|
||||
},
|
||||
"file-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13 4H7.5A1.5 1.5 0 0 0 6 5.5v13A1.5 1.5 0 0 0 7.5 20h9a1.5 1.5 0 0 0 1.5-1.5V9h-.004zm-1 1.604V9.75c0 .138.112.25.25.25h4.147a.25.25 0 0 0 .176-.427l-4.146-4.146a.25.25 0 0 0-.427.177\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"file-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M6.5 6A1.5 1.5 0 0 1 8 4.5h5.5l4 4V18a1.5 1.5 0 0 1-1.5 1.5H8A1.5 1.5 0 0 1 6.5 18z\"/><path stroke-linejoin=\"round\" d=\"M13 4.5V9h4.5\"/></g>"
|
||||
},
|
||||
"flip-horizontal-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M15.079 3.462a.75.75 0 0 1 1.06.016l3.399 3.5a.75.75 0 0 1 0 1.045l-3.398 3.5a.75.75 0 1 1-1.077-1.045l2.163-2.228H5a.75.75 0 1 1 0-1.5h12.226l-2.163-2.227a.75.75 0 0 1 .016-1.06m-6.158 9a.75.75 0 0 1 .015 1.06L6.773 15.75H19a.75.75 0 0 1 0 1.5H6.774l2.162 2.227a.75.75 0 0 1-1.076 1.045l-3.398-3.5a.75.75 0 0 1 0-1.045l3.398-3.5a.75.75 0 0 1 1.06-.015\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"flip-horizontal-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 16.5h13M8.398 20L5 16.5L8.398 13M18 7.5H5M15.602 11L19 7.5L15.6 4\"/>"
|
||||
},
|
||||
"flip-vertical-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M7.5 4.25a.75.75 0 0 1 .75.75v12.227l2.227-2.163a.75.75 0 1 1 1.045 1.076l-3.5 3.398a.75.75 0 0 1-1.045 0l-3.5-3.398a.75.75 0 1 1 1.045-1.076l2.228 2.162V5a.75.75 0 0 1 .75-.75m8.477.212a.75.75 0 0 1 1.045 0l3.5 3.398a.75.75 0 0 1-1.045 1.076L17.25 6.774V19a.75.75 0 0 1-1.5 0V6.774l-2.228 2.162a.75.75 0 0 1-1.045-1.076z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"flip-vertical-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.5 18V5M4 15.602L7.5 19l3.5-3.398M16.5 6v13M13 8.398L16.5 5L20 8.398\"/>"
|
||||
},
|
||||
"gear-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m17.653 10.35l2.056.336a.35.35 0 0 1 .291.343v1.913a.35.35 0 0 1-.288.342l-2.058.362a5.8 5.8 0 0 1-.49 1.185l1.217 1.69a.35.35 0 0 1-.036.45l-1.353 1.352a.35.35 0 0 1-.445.04l-1.707-1.198q-.566.315-1.193.5l-.363 2.048a.35.35 0 0 1-.343.287h-1.913a.35.35 0 0 1-.343-.29l-.34-2.039a6 6 0 0 1-1.197-.495l-1.694 1.186a.35.35 0 0 1-.446-.038L5.655 16.97a.35.35 0 0 1-.036-.448l1.197-1.674a6 6 0 0 1-.5-1.204l-2.029-.36A.35.35 0 0 1 4 12.942v-1.913c0-.17.123-.315.29-.343l2.03-.338q.183-.63.5-1.203L5.636 7.453a.35.35 0 0 1 .039-.445l1.352-1.352c.12-.12.31-.136.448-.037l1.68 1.2a6 6 0 0 1 1.196-.491l.333-2.036A.35.35 0 0 1 11.028 4h1.913c.17 0 .314.122.343.288l.359 2.047q.624.182 1.193.497l1.685-1.211a.35.35 0 0 1 .45.036l1.352 1.352c.12.12.136.308.039.446l-1.2 1.71q.31.565.49 1.185M9.565 12a2.435 2.435 0 1 0 4.87 0a2.435 2.435 0 0 0-4.87 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"gear-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"m17.3 10.453l1.927.315a.326.326 0 0 1 .273.322v1.793a.326.326 0 0 1-.27.321l-1.93.339q-.167.582-.459 1.111l1.141 1.584a.326.326 0 0 1-.034.422l-1.268 1.268a.326.326 0 0 1-.418.037l-1.6-1.123a5.5 5.5 0 0 1-1.118.468l-.34 1.921a.326.326 0 0 1-.322.269H11.09a.325.325 0 0 1-.321-.272l-.319-1.911a5.5 5.5 0 0 1-1.123-.465l-1.588 1.113a.326.326 0 0 1-.418-.037L6.052 16.66a.33.33 0 0 1-.035-.42l1.123-1.57a5.5 5.5 0 0 1-.47-1.129l-1.901-.337a.326.326 0 0 1-.269-.321V11.09c0-.16.115-.296.273-.322l1.901-.317q.173-.59.47-1.128l-1.11-1.586a.326.326 0 0 1 .037-.417L7.34 6.053a.326.326 0 0 1 .42-.035l1.575 1.125q.533-.292 1.121-.46l.312-1.91a.326.326 0 0 1 .322-.273h1.793c.159 0 .294.114.322.27l.336 1.92q.585.169 1.12.465l1.578-1.135a.326.326 0 0 1 .422.033l1.268 1.268a.326.326 0 0 1 .036.418L16.84 9.342q.29.53.46 1.11ZM9.716 12a2.283 2.283 0 1 0 4.566 0a2.283 2.283 0 0 0-4.566 0Z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"globe-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M10.51 6.617c-.518 1.341-.855 3.245-.855 5.385s.337 4.044.856 5.385c.26.673.552 1.166.838 1.478s.506.385.651.385s.366-.073.652-.385c.285-.312.577-.805.837-1.478c.52-1.341.856-3.245.856-5.385s-.337-4.044-.856-5.385c-.26-.673-.552-1.166-.838-1.478s-.506-.385-.651-.385s-.366.073-.651.385s-.578.805-.838 1.478m-.268-2.49c.455-.499 1.048-.873 1.758-.873s1.303.374 1.758.872c.455.497.83 1.175 1.13 1.95c.601 1.553.957 3.649.957 5.926s-.356 4.373-.957 5.926c-.3.774-.675 1.453-1.13 1.95s-1.048.872-1.758.872s-1.303-.375-1.758-.872s-.83-1.175-1.13-1.95c-.601-1.553-.957-3.649-.957-5.926s.356-4.373.957-5.926c.3-.775.675-1.453 1.13-1.95\"/><path d=\"M12 4.746a7.25 7.25 0 1 0 0 14.5a7.25 7.25 0 0 0 0-14.5m-8.75 7.25a8.75 8.75 0 1 1 17.5 0a8.75 8.75 0 0 1-17.5 0\"/><path d=\"M3.25 11.99a.75.75 0 0 1 .75-.75l16 .012a.75.75 0 1 1 0 1.5l-16-.011a.75.75 0 0 1-.75-.75\"/></g>"
|
||||
},
|
||||
"globe-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><ellipse cx=\"12\" cy=\"12.006\" rx=\"3.095\" ry=\"7.998\"/><circle cx=\"12\" cy=\"12\" r=\"8\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 11.995l16 .01\"/></g>"
|
||||
},
|
||||
"graph-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M5.5 18a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/><rect width=\"3\" height=\"7\" x=\"6.5\" y=\"11.5\" fill=\"currentColor\" rx=\".5\"/><rect width=\"3\" height=\"13\" x=\"10.5\" y=\"5.5\" fill=\"currentColor\" rx=\".5\"/><rect width=\"3\" height=\"10\" x=\"14.5\" y=\"8.5\" fill=\"currentColor\" rx=\".5\"/>"
|
||||
},
|
||||
"graph-outline": {
|
||||
"body": "<defs><path id=\"bitcoinIconsGraphOutline0\" d=\"M18 18H6\"/><path id=\"bitcoinIconsGraphOutline1\" d=\"M7 12h2v6H7zm4-6h2v12h-2zm4 3h2v9h-2z\"/></defs><g fill=\"none\" stroke=\"currentColor\"><use href=\"#bitcoinIconsGraphOutline0\" stroke-linecap=\"round\"/><use href=\"#bitcoinIconsGraphOutline1\" stroke-linejoin=\"round\"/><use href=\"#bitcoinIconsGraphOutline0\" stroke-linecap=\"round\"/><use href=\"#bitcoinIconsGraphOutline1\" stroke-linejoin=\"round\"/></g>"
|
||||
},
|
||||
"grid-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M5 10.2V5.8a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8v4.4a.8.8 0 0 1-.8.8H5.8a.8.8 0 0 1-.8-.8m8 0V5.8a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8v4.4a.8.8 0 0 1-.8.8h-4.4a.8.8 0 0 1-.8-.8m0 8v-4.4a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8v4.4a.8.8 0 0 1-.8.8h-4.4a.8.8 0 0 1-.8-.8m-8 0v-4.4a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8v4.4a.8.8 0 0 1-.8.8H5.8a.8.8 0 0 1-.8-.8\"/>"
|
||||
},
|
||||
"grid-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.5 9.5v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1m8 0v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1m0 8v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1m-8 0v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1\"/>"
|
||||
},
|
||||
"hidden-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M14.33 7.17A16 16 0 0 0 12 7c-4.97 0-9 2.239-9 5c0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 0 1 5.076-5.076zm-3.1 8.756q.375.074.77.074a4 4 0 0 0 3.926-4.77l2.647-2.646C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5q-.899 0-1.749-.094zm6.563-10.719a1 1 0 1 1 1.414 1.414L6.48 19.35a1 1 0 1 1-1.414-1.414z\"/>"
|
||||
},
|
||||
"hidden-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M10.563 16.436q.701.064 1.437.064c4.694 0 8.5-2.015 8.5-4.5c0-1.213-.906-2.313-2.379-3.122m-4.685-1.314A16 16 0 0 0 12 7.5c-4.694 0-8.5 2.015-8.5 4.5c0 1.212.905 2.312 2.376 3.121\"/><path stroke-linejoin=\"round\" d=\"M19 5L5 19\"/></g>"
|
||||
},
|
||||
"home-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.827 3.834a1.5 1.5 0 0 0-2.26.054L5 10.58v8.92A1.5 1.5 0 0 0 6.5 21h1.79a1.5 1.5 0 0 0 1.5-1.5v-3.011a1.5 1.5 0 0 1 1.5-1.5h1.42a1.5 1.5 0 0 1 1.5 1.5V19.5a1.5 1.5 0 0 0 1.5 1.5h1.79a1.5 1.5 0 0 0 1.5-1.5v-8.92z\"/>"
|
||||
},
|
||||
"home-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M12.391 4.262a1 1 0 0 0-1.46.035l-6.177 6.919a1 1 0 0 0-.254.666V19.5a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V16a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v3.5a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-7.591a1 1 0 0 0-.287-.7z\"/>"
|
||||
},
|
||||
"info-circle-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-7.75-4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0M13 17.5v-7h-2v7z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"info-circle-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path stroke-linecap=\"round\" d=\"M12 10.5v7M12 8V7\"/></g>"
|
||||
},
|
||||
"info-filled": {
|
||||
"body": "<circle cx=\"12\" cy=\"7.25\" r=\"1.25\" fill=\"currentColor\"/><path fill=\"currentColor\" d=\"M11 10h2v8h-2z\"/>"
|
||||
},
|
||||
"info-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M12 10.5v7M12 8V7\"/>"
|
||||
},
|
||||
"invoice-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M7.5 4A1.5 1.5 0 0 0 6 5.5v13A1.5 1.5 0 0 0 7.5 20h9a1.5 1.5 0 0 0 1.5-1.5v-13A1.5 1.5 0 0 0 16.5 4zm6.854 4.354a.5.5 0 0 0-.708-.708l-4 4a.5.5 0 0 0 .708.708zM11.5 8.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m2 4a1 1 0 1 0 0-2a1 1 0 0 0 0 2m-5 2.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1H9a.5.5 0 0 1-.5-.5m.5 1.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"invoice-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"11\" height=\"15\" x=\"6.5\" y=\"4.5\" rx=\"1.5\"/><path stroke-linecap=\"round\" d=\"m10 12l4-4\"/><circle cx=\"10.5\" cy=\"8.5\" r=\".5\"/><circle cx=\"13.5\" cy=\"11.5\" r=\".5\"/><path stroke-linecap=\"round\" d=\"M9 15h6m-6 2h6\"/></g>"
|
||||
},
|
||||
"key-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.865 7.374c.74 2.968-1.163 5.998-4.25 6.767a6 6 0 0 1-1.155.172l-3.435 5.471a2 2 0 0 1-1.21.877l-1.974.492a1 1 0 0 1-1.212-.728l-.445-1.786a2 2 0 0 1 .246-1.547l3.157-5.028a5.3 5.3 0 0 1-.9-1.903c-.74-2.968 1.162-5.998 4.249-6.768s6.189 1.013 6.929 3.98m-4.627 1.324c.685-.171 1.108-.844.944-1.504s-.854-1.055-1.54-.884s-1.109.844-.944 1.503c.164.66.854 1.056 1.54.885\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"key-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M18.865 7.374c.74 2.968-1.163 5.998-4.25 6.767a6 6 0 0 1-1.155.172l-3.435 5.471a2 2 0 0 1-1.21.877l-1.974.492a1 1 0 0 1-1.212-.728l-.445-1.786a2 2 0 0 1 .246-1.547l3.157-5.028a5.3 5.3 0 0 1-.9-1.903c-.74-2.968 1.162-5.998 4.249-6.768s6.189 1.013 6.929 3.98Zm-5.157 2.991a2 2 0 1 0-.967-3.881a2 2 0 0 0 .967 3.881Z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"lightning-circle-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 21a9 9 0 1 0 0-18a9 9 0 0 0 0 18m-1.58-3.89l6.215-5.966a.2.2 0 0 0-.14-.344l-4.519.052a.2.2 0 0 1-.184-.283l1.654-3.64c.09-.2-.16-.378-.32-.227l-5.76 5.448a.2.2 0 0 0 .137.345h4.641a.2.2 0 0 1 .176.295l-2.215 4.08c-.11.2.15.397.314.24\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"lightning-circle-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m16.778 10.965l-6.286 6.083c-.208.2-.534-.051-.392-.302l2.295-4.06a.125.125 0 0 0-.11-.186H7.31a.125.125 0 0 1-.087-.215l5.728-5.524c.203-.195.523.04.397.292l-1.758 3.516a.125.125 0 0 0 .112.181h4.99c.111 0 .167.137.086.215Z\"/></g>"
|
||||
},
|
||||
"lightning-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"m18.496 10.709l-8.636 8.88c-.24.246-.638-.039-.482-.345l3.074-6.066a.3.3 0 0 0-.268-.436H5.718a.3.3 0 0 1-.214-.51l8.01-8.115c.232-.235.618.023.489.328L11.706 9.86a.3.3 0 0 0 .28.417l6.291-.078a.3.3 0 0 1 .22.509\"/>"
|
||||
},
|
||||
"lightning-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"m18.496 10.709l-8.636 8.88c-.24.246-.638-.039-.482-.345l3.074-6.066a.3.3 0 0 0-.268-.436H5.718a.3.3 0 0 1-.214-.51l8.01-8.115c.232-.235.618.023.489.328L11.706 9.86a.3.3 0 0 0 .28.417l6.291-.078a.3.3 0 0 1 .22.509Z\"/>"
|
||||
},
|
||||
"link-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"m7.05 11.293l-2.12 2.121a4 4 0 0 0 5.657 5.657l2.828-2.828a4 4 0 0 0 0-5.657l-1.06 1.06a2.5 2.5 0 0 1 0 3.536l-2.83 2.828a2.5 2.5 0 0 1-3.535-3.535l2.12-2.121z\"/><path fill=\"currentColor\" d=\"m15.889 11.646l2.121-2.12a2.5 2.5 0 0 0-3.535-3.536l-2.829 2.828a2.5 2.5 0 0 0 0 3.536l-1.06 1.06a4 4 0 0 1 0-5.657l2.828-2.828a4 4 0 0 1 5.657 5.657l-2.121 2.121z\"/>"
|
||||
},
|
||||
"link-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.576 13.481a3.5 3.5 0 0 0 4.95 4.95m2.473-7.423a3.5 3.5 0 0 1 0 4.95l-2.475 2.475m-2.475-7.425l-2.475 2.475m12.857-2.957a3.5 3.5 0 1 0-4.95-4.95M11.01 13a3.5 3.5 0 0 1 0-4.95l2.474-2.475M15.958 13l2.475-2.475\"/>"
|
||||
},
|
||||
"linux-terminal-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M9.25 19a.75.75 0 0 1 .75-.75h10.25a.75.75 0 0 1 0 1.5H10a.75.75 0 0 1-.75-.75m-2.645-2.5a.75.75 0 0 1 .75.75V19a.75.75 0 0 1-1.5 0v-1.75a.75.75 0 0 1 .75-.75m0-12a.75.75 0 0 1 .75.75V7a.75.75 0 1 1-1.5 0V5.25a.75.75 0 0 1 .75-.75\"/><path d=\"M2.965 9.51c0-1.907 1.753-3.208 3.707-3.208c1.925 0 3.449 1.143 3.746 2.456a.75.75 0 1 1-1.464.33c-.103-.457-.865-1.286-2.282-1.286c-1.39 0-2.207.87-2.207 1.707c0 .257.06.428.145.558c.09.138.233.272.456.404c.45.267 1.061.44 1.816.654l.138.04c.755.215 1.655.48 2.356.97c.76.532 1.289 1.33 1.289 2.503c0 .968-.578 1.772-1.302 2.302a4.62 4.62 0 0 1-2.691.862c-1.995 0-3.446-1.237-3.888-2.655a.75.75 0 1 1 1.432-.447c.244.782 1.107 1.602 2.456 1.602c.678 0 1.333-.227 1.805-.573c.48-.35.688-.753.688-1.091c0-.634-.247-.992-.65-1.274c-.46-.323-1.111-.53-1.906-.757l-.19-.053c-.691-.196-1.49-.421-2.118-.793a2.8 2.8 0 0 1-.95-.877a2.5 2.5 0 0 1-.386-1.375\"/></g>"
|
||||
},
|
||||
"linux-terminal-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path stroke-linejoin=\"round\" d=\"M10 19h10.5\"/><path d=\"M6.605 17.25V19m0-13.75V7\"/><path stroke-linejoin=\"round\" d=\"M9.686 8.923c-.2-.885-1.343-1.871-3.015-1.871c-1.671 0-2.957 1.086-2.957 2.457c0 3.114 6.2 1.514 6.2 5.129c0 1.306-1.571 2.414-3.243 2.414c-1.67 0-2.828-1.029-3.171-2.129\"/></g>"
|
||||
},
|
||||
"lock-filled": {
|
||||
"body": "<rect width=\"14\" height=\"10\" x=\"5\" y=\"11\" fill=\"currentColor\" rx=\"2\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M9.5 6.75a.25.25 0 0 0-.25.25v5h-1.5V7c0-.966.784-1.75 1.75-1.75h5c.966 0 1.75.784 1.75 1.75v5h-1.5V7a.25.25 0 0 0-.25-.25z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"lock-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"14\" height=\"10\" x=\"5\" y=\"10.968\" rx=\"2\"/><path d=\"M15.486 10.984V7.243a1.5 1.5 0 0 0-1.5-1.5h-3.972a1.5 1.5 0 0 0-1.5 1.5v3.74\"/></g>"
|
||||
},
|
||||
"magic-wand-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m7.999 20l7-7.003a2.822 2.822 0 0 0-3.99-3.993l-7.007 6.997a2.827 2.827 0 1 0 3.997 4M11 11l2 2l1-1a1.414 1.414 0 1 0-2-2z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M6.759 4.897c.066-.247.416-.247.482 0L7.6 6.225a.25.25 0 0 0 .176.176l1.328.358c.247.066.247.416 0 .482L7.775 7.6a.25.25 0 0 0-.176.176L7.24 9.103c-.066.247-.416.247-.482 0L6.4 7.775a.25.25 0 0 0-.176-.176l-1.329-.358c-.246-.066-.246-.416 0-.482l1.33-.359a.25.25 0 0 0 .176-.176zm10-1c.066-.247.416-.247.482 0l.358 1.328a.25.25 0 0 0 .176.176l1.328.358c.247.066.247.416 0 .482l-1.328.358a.25.25 0 0 0-.176.176l-.358 1.328c-.066.247-.416.247-.482 0L16.4 6.775a.25.25 0 0 0-.177-.176l-1.328-.358c-.246-.066-.246-.416 0-.482l1.328-.358a.25.25 0 0 0 .177-.176zm1 9c.066-.247.416-.247.482 0l.358 1.328a.25.25 0 0 0 .176.176l1.328.358c.247.066.247.416 0 .482l-1.328.358a.25.25 0 0 0-.176.176l-.358 1.328c-.066.247-.416.247-.482 0l-.358-1.328a.25.25 0 0 0-.177-.176l-1.328-.358c-.246-.066-.246-.416 0-.482l1.328-.358a.25.25 0 0 0 .177-.176z\"/>"
|
||||
},
|
||||
"magic-wand-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"m14.75 12.746l-7 7.004a2.475 2.475 0 1 1-3.5-3.5l7.002-7.002a2.474 2.474 0 0 1 3.499 3.498Zm-4.717-2.23l3.451 3.45\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M18 13.25v3.5M19.75 15h-3.5M17 4.25v3.5M18.75 6h-3.5M7 5.25v3.5M8.75 7h-3.5\"/></g>"
|
||||
},
|
||||
"menu-filled": {
|
||||
"body": "<rect width=\"18\" height=\"1.5\" x=\"3\" y=\"7.001\" fill=\"currentColor\" rx=\".75\"/><rect width=\"15\" height=\"1.5\" x=\"3\" y=\"11.251\" fill=\"currentColor\" rx=\".75\"/><rect width=\"18\" height=\"1.5\" x=\"3\" y=\"15.499\" fill=\"currentColor\" rx=\".75\"/>"
|
||||
},
|
||||
"menu-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M3.5 7.5h17M3.5 12h14m-14 4.5h17\"/>"
|
||||
},
|
||||
"message-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8.5 18.896V16H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-6l-3.073 3.073a.25.25 0 0 1-.427-.177M8 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M8.5 9a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"message-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M8.5 18.396V15.5h-2a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H12l-3.073 3.073a.25.25 0 0 1-.427-.177Z\"/><path stroke-linecap=\"round\" d=\"M8.5 12.5h7m-7-3h7\"/></g>"
|
||||
},
|
||||
"milk-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M14.278 4.977a.95.95 0 0 0-.832-.477h-2.858a.96.96 0 0 0-.861.518c-.13.253-.296.589-.48.982h5.569a22 22 0 0 0-.538-1.023\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M7.5 12c0-1.542.66-3.48 1.302-5h6.475c.616 1.43 1.223 3.275 1.223 5v7a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2zM9 12a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 1 0v-6A.5.5 0 0 0 9 12\" clip-rule=\"evenodd\"/><rect width=\"7\" height=\"3\" x=\"8.5\" y=\"3\" fill=\"currentColor\" rx=\"1\"/>"
|
||||
},
|
||||
"milk-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M14.054 5.985a.94.94 0 0 0-.836-.485h-2.402a.96.96 0 0 0-.865.526c-.566 1.134-1.83 3.877-1.943 5.97a.004.004 0 0 1-.004.004a.004.004 0 0 0-.004.004V18.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-6.497L15.997 12l-.003-.003c-.1-2.39-1.368-4.965-1.94-6.012Z\"/><rect width=\"6\" height=\"2\" x=\"9\" y=\"3.5\" rx=\".5\"/><path stroke-linecap=\"round\" d=\"M10 12.5V18\"/></g>"
|
||||
},
|
||||
"miner-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M5.642 18.37a1.224 1.224 0 0 1 0-1.74A8.97 8.97 0 0 1 12 14a8.97 8.97 0 0 1 6.358 2.63a1.224 1.224 0 0 1 0 1.74A8.97 8.97 0 0 1 12 21a8.97 8.97 0 0 1-6.358-2.63\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8.5 7.5h7V8h.5c0 2.637-1.681 5-4 5s-4-2.363-4-5h.5zm.523 1C9.213 10.568 10.566 12 12 12s2.787-1.432 2.977-3.5z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 3c2.485 0 4.5 2.239 4.5 5h-9c0-2.761 2.015-5 4.5-5m0 4.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m-5.5 1A.5.5 0 0 1 7 8h10a.5.5 0 1 1 0 1H7a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"miner-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M6.413 18.406a1.197 1.197 0 0 1 0-1.812A8.47 8.47 0 0 1 12 14.5c2.139 0 4.093.79 5.587 2.094a1.197 1.197 0 0 1 0 1.812A8.47 8.47 0 0 1 12 20.5a8.47 8.47 0 0 1-5.587-2.094ZM8.521 8.5c.194 2.25 1.677 4 3.479 4s3.285-1.75 3.479-4z\"/><path d=\"M16 8q0 .254-.024.5H8.024A5 5 0 0 1 8 8c0-2.485 1.79-4.5 4-4.5s4 2.015 4 4.5Zm-4-1a1 1 0 1 0 0-2a1 1 0 0 0 0 2Z\" clip-rule=\"evenodd\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7 8.5h10\"/></g>"
|
||||
},
|
||||
"mining-device-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M8.5 12c0-.786.26-1.512.697-2.096L11.293 12l-2.096 2.096A3.5 3.5 0 0 1 8.5 12m5.596-2.803A3.5 3.5 0 0 0 12 8.5c-.786 0-1.512.26-2.096.697L12 11.293zM12 12.707l2.096 2.096A3.5 3.5 0 0 1 12 15.5c-.786 0-1.512-.26-2.096-.697zm.707-.707l2.096-2.096c.438.584.697 1.31.697 2.096s-.26 1.512-.697 2.096z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M19 5H5v14h14zM8.484 14.809l-1.338 1.338a.5.5 0 1 0 .708.707l1.337-1.338A4.48 4.48 0 0 0 12 16.5a4.48 4.48 0 0 0 2.809-.984l1.337 1.338a.5.5 0 0 0 .708-.707l-1.338-1.338A4.48 4.48 0 0 0 16.5 12a4.48 4.48 0 0 0-.984-2.809l1.338-1.337a.5.5 0 0 0-.708-.708L14.81 8.484A4.48 4.48 0 0 0 12 7.5a4.48 4.48 0 0 0-2.809.984L7.854 7.146a.5.5 0 1 0-.708.708L8.484 9.19A4.48 4.48 0 0 0 7.5 12a4.48 4.48 0 0 0 .984 2.809\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"mining-device-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M5.5 5.5h13v13h-13z\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m7.5 7.5l9 9m0-9l-9 9\"/></g>"
|
||||
},
|
||||
"mining-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.101 2.9a.5.5 0 0 1 .378-.056l3.397.847a.5.5 0 0 1 .37.573c2.365 1.107 4.125 2.93 4.815 4.983a.5.5 0 0 1-.82.52c-1.21-1.16-2.794-2.171-4.642-2.902l-.025.1a.5.5 0 0 1-.606.364l-.485-.12l-.907 3.638a.5.5 0 0 1 .364.606l-2.298 9.218a.5.5 0 0 1-.606.364L8.61 20.43a.5.5 0 0 1-.364-.606l2.298-9.218a.5.5 0 0 1 .606-.364l.907-3.638l-.485-.121a.5.5 0 0 1-.364-.607l.025-.1c-1.974-.222-3.847-.073-5.46.383a.5.5 0 0 1-.48-.844c1.572-1.488 3.982-2.272 6.59-2.14a.5.5 0 0 1 .218-.274\"/>"
|
||||
},
|
||||
"mining-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12.265 3.703c-2.536-.225-4.88.459-6.423 1.79c-.19.164-.02.443.226.385c1.717-.41 3.67-.494 5.704-.197zm2.903 2.824c1.935.693 3.62 1.685 4.944 2.853c.189.166.472 0 .38-.235c-.736-1.899-2.486-3.603-4.83-4.595zm-2.687-.591l1.94.484l-1.209 4.851l-1.94-.484zm-1.694 4.731l2.91.726L11.4 20.61l-2.911-.726z\"/><path d=\"m12.358 3.329l3.396.847l-.665 2.668l-3.396-.847z\"/></g>"
|
||||
},
|
||||
"minus-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M4.85 11.25h14.302a.75.75 0 1 1 0 1.5H4.85a.75.75 0 0 1 0-1.5\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"minus-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M19.5 12h-15\"/>"
|
||||
},
|
||||
"mixed-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M8.504 15.7c-1.172 1.09-2.594 1.873-4.513 1.85a.75.75 0 0 1 .018-1.5c1.435.018 2.502-.545 3.473-1.449c.778-.724 1.451-1.62 2.187-2.6q.33-.442.684-.903c.994-1.288 2.139-2.62 3.69-3.514c1.577-.909 3.515-1.333 6.037-.924a.75.75 0 1 1-.24 1.48c-2.21-.357-3.794.021-5.049.744c-1.28.738-2.276 1.867-3.25 3.13q-.307.4-.62.818c-.75 1-1.531 2.043-2.417 2.867\"/><path d=\"M16.04 3.469a.75.75 0 0 1 1.06.001l3.431 3.44a.75.75 0 0 1 0 1.06l-3.432 3.44a.75.75 0 0 1-1.062-1.06l2.904-2.91l-2.903-2.91a.75.75 0 0 1 .001-1.061m.016 9.185a.75.75 0 0 1 1.06.032l3.39 3.6a.75.75 0 0 1-.03 1.06l-3.391 3.2a.75.75 0 0 1-1.03-1.091l2.846-2.686l-2.877-3.055a.75.75 0 0 1 .032-1.06\"/><path d=\"M7.53 9.396C6.574 8.49 5.496 7.923 4.014 7.95a.75.75 0 0 1-.026-1.5c1.962-.035 3.403.748 4.575 1.856c.921.872 1.71 1.983 2.46 3.04q.255.36.506.709c.936 1.29 1.876 2.441 3.094 3.2c1.187.74 2.692 1.14 4.822.804a.75.75 0 0 1 .233 1.482c-2.459.387-4.333-.068-5.848-1.013c-1.485-.925-2.569-2.287-3.515-3.593q-.285-.393-.553-.773c-.745-1.048-1.426-2.006-2.23-2.766\"/></g>"
|
||||
},
|
||||
"mixed-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 16.8c6.708.082 6.496-10.932 15.96-9.4\"/><path d=\"M16.57 4L20 7.44l-3.432 3.44m.002 2.32l3.39 3.6l-3.39 3.2\"/><path d=\"M4 7.2c6.887-.124 6.381 11.044 15.56 9.6\"/></g>"
|
||||
},
|
||||
"mnemonic-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M5 6.5A1.5 1.5 0 0 1 6.5 5h11A1.5 1.5 0 0 1 19 6.5v11a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 17.5zm1.5 0H9V8H6.5zM9 9.667H6.5v1.5H9zm-2.5 3.166H9v1.5H6.5zM9 16H6.5v1.5H9zm1.75-9.5h2.5V8h-2.5zm2.5 3.167h-2.5v1.5h2.5zm-2.5 3.166h2.5v1.5h-2.5zM13.25 16h-2.5v1.5h2.5zM15 6.5h2.5V8H15zm2.5 3.167H15v1.5h2.5zM15 12.833h2.5v1.5H15zM17.5 16H15v1.5h2.5z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"mnemonic-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"13\" height=\"13\" x=\"5.5\" y=\"5.5\" rx=\"1\"/><path stroke-linecap=\"round\" d=\"M15 16h1.5m-5 0H13m-5 0h1.5m5.5-2.667h1.5m-5 0H13m-5 0h1.5m5.5-2.666h1.5m-5 0H13m-5 0h1.5M15 8h1.5m-5 0H13M8 8h1.5\"/></g>"
|
||||
},
|
||||
"moon-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M20.993 13.313a6 6 0 0 1-7.306-7.306a7 7 0 1 0 7.306 7.306\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M4.5 8.25a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.25 9.5a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1h-1.5a.5.5 0 0 1-.5-.5M7.5 3a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2a.5.5 0 0 1 .5-.5\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6 4.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"moon-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M20.434 13.934a6.5 6.5 0 0 1-7.367-7.367A6.501 6.501 0 0 0 14 19.5a6.5 6.5 0 0 0 6.433-5.566Z\"/><path stroke-linecap=\"round\" d=\"M4.5 8.75v1.5m.75-.75h-1.5m3.75-6v2m1-1h-2\"/></g>"
|
||||
},
|
||||
"no-dollars-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M18.509 18.216A9 9 0 1 1 5.784 5.49l3.097 3.097a2.5 2.5 0 0 0-.224 1.06c0 1.352.98 2.156 2.549 2.607l.392.113v3.71c-1.03-.108-1.872-.637-1.96-1.647H8.383c.113 1.544 1.289 2.622 3.215 2.75v1.093h.804V17.18c1.64-.114 2.707-.908 3.073-1.999zm.651-.763l-3.574-3.574c-.193-1.248-1.251-1.841-2.158-2.158l-1.026-1.026V7.937c1.039.103 1.794.662 1.902 1.554h1.176c-.05-1.432-1.304-2.52-3.078-2.637V5.726h-.804v1.142c-.853.087-1.598.405-2.124.899L6.547 4.84A9 9 0 0 1 19.16 17.454\"/><path fill=\"currentColor\" d=\"M9.834 9.541v.048c0 .933.897 1.353 1.624 1.576zm1.764.349l-1.337-1.336c.308-.312.768-.527 1.337-.602zm1.512 2.928a9 9 0 0 0-.669-.21l-.04-.01v3.48c1.187-.133 2.04-.819 2.04-1.804q0-.068-.006-.132z\"/>"
|
||||
},
|
||||
"no-dollars-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path stroke-linejoin=\"round\" d=\"m5.75 5.75l12.5 12.5\"/><path stroke-linecap=\"round\" d=\"M12 6v12\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.567 9.37c-.17-.755-1.145-1.595-2.57-1.595s-2.52.925-2.52 2.094c0 2.655 5.285 1.291 5.285 4.372c0 1.114-1.34 2.059-2.765 2.059s-2.411-.877-2.703-1.815\"/></g>"
|
||||
},
|
||||
"node-0-connections-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.54 7.165c.5-.162.901-.543 1.091-1.03l4.233 4.234a1.76 1.76 0 0 0-1.03 1.092zm4.295 5.375l-4.296 4.295c.5.162.902.543 1.092 1.03l4.233-4.234a1.76 1.76 0 0 1-1.03-1.092m-5.374 4.296l-4.295-4.296c-.162.5-.542.902-1.03 1.092l4.234 4.233c.19-.487.591-.867 1.092-1.03m-.001-9.669l-4.295 4.296a1.76 1.76 0 0 0-1.03-1.092l4.234-4.233c.19.487.591.867 1.092 1.03\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 4.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m2.5 12a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m-4-7.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2M3 12a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m15.5-1a1 1 0 1 0 0 2a1 1 0 0 0 0-2M16 12a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"node-0-connections-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"m13.5 7l3.5 3.5m-10 3l3.5 3.5m0-10L7 10.5m10 3L13.5 17\"/><circle cx=\"12\" cy=\"5.5\" r=\"2\"/><circle cx=\"12\" cy=\"18.5\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\"2\"/><circle cx=\"18.5\" cy=\"12\" r=\"2\"/></g>"
|
||||
},
|
||||
"node-1-connection-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.54 7.165c.5-.162.901-.543 1.091-1.03l4.233 4.234a1.76 1.76 0 0 0-1.03 1.092zm4.295 5.375l-4.296 4.295c.5.162.902.543 1.092 1.03l4.233-4.234a1.76 1.76 0 0 1-1.03-1.092m-5.374 4.296l-4.295-4.296c-.162.5-.542.902-1.03 1.092l4.234 4.233c.19-.487.591-.867 1.092-1.03m-.001-9.669l-4.295 4.296a1.76 1.76 0 0 0-1.03-1.092l4.234-4.233c.19.487.591.867 1.092 1.03\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 4.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m2.5 12a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/><circle cx=\"5.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.5 11a1 1 0 1 0 0 2a1 1 0 0 0 0-2M16 12a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"node-1-connection-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"m13.5 7l3.5 3.5m-10 3l3.5 3.5m0-10L7 10.5m10 3L13.5 17\"/><circle cx=\"12\" cy=\"5.5\" r=\"2\"/><circle cx=\"12\" cy=\"18.5\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\"2\"/><circle cx=\"18.5\" cy=\"12\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\".5\"/></g>"
|
||||
},
|
||||
"node-2-connections-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.54 7.165c.5-.162.901-.543 1.091-1.03l4.233 4.234a1.76 1.76 0 0 0-1.03 1.092zm4.295 5.375l-4.296 4.295c.5.162.902.543 1.092 1.03l4.233-4.234a1.76 1.76 0 0 1-1.03-1.092m-5.374 4.296l-4.295-4.296c-.162.5-.542.902-1.03 1.092l4.234 4.233c.19-.487.591-.867 1.092-1.03m-.001-9.669l-4.295 4.296a1.76 1.76 0 0 0-1.03-1.092l4.234-4.233c.19.487.591.867 1.092 1.03\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 4.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/><circle cx=\"12\" cy=\"18.5\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"5.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.5 11a1 1 0 1 0 0 2a1 1 0 0 0 0-2M16 12a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"node-2-connections-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"m13.5 7l3.5 3.5m-10 3l3.5 3.5m0-10L7 10.5m10 3L13.5 17\"/><circle cx=\"12\" cy=\"5.5\" r=\"2\"/><circle cx=\"12\" cy=\"18.5\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\"2\"/><circle cx=\"18.5\" cy=\"12\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\".5\"/><circle cx=\"12\" cy=\"18.5\" r=\".5\"/></g>"
|
||||
},
|
||||
"node-3-connections-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12.54 7.165c.5-.162.901-.543 1.091-1.03l4.233 4.234a1.76 1.76 0 0 0-1.03 1.092zm4.295 5.375l-4.296 4.295c.5.162.902.543 1.092 1.03l4.233-4.234a1.76 1.76 0 0 1-1.03-1.092m-5.374 4.296l-4.295-4.296c-.162.5-.542.902-1.03 1.092l4.234 4.233c.19-.487.591-.867 1.092-1.03m-.001-9.669l-4.295 4.296a1.76 1.76 0 0 0-1.03-1.092l4.234-4.233c.19.487.591.867 1.092 1.03\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 4.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2m-2.5 1a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0\" clip-rule=\"evenodd\"/><circle cx=\"12\" cy=\"18.5\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"5.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"18.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/>"
|
||||
},
|
||||
"node-3-connections-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"m13.5 7l3.5 3.5m-10 3l3.5 3.5m0-10L7 10.5m10 3L13.5 17\"/><circle cx=\"12\" cy=\"5.5\" r=\"2\"/><circle cx=\"12\" cy=\"18.5\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\"2\"/><circle cx=\"18.5\" cy=\"12\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\".5\"/><circle cx=\"12\" cy=\"18.5\" r=\".5\"/><circle cx=\"18.5\" cy=\"12\" r=\".5\"/></g>"
|
||||
},
|
||||
"node-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m12.31 4.815l7.496 7.496l-7.495 7.495l-7.496-7.495zm-5.373 7.496l5.374 5.374l5.374-5.374l-5.374-5.374z\" clip-rule=\"evenodd\"/><circle cx=\"12\" cy=\"5.5\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"18.5\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"5.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/><circle cx=\"18.5\" cy=\"12\" r=\"2.5\" fill=\"currentColor\"/>"
|
||||
},
|
||||
"node-hardware-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 9.884a.73.73 0 0 0-.364-.646l-7.494-4.395a2.21 2.21 0 0 0-2.23-.003L3.368 9.237a.73.73 0 0 0-.365.647H3v4.43h.004a.73.73 0 0 0 .376.621l7.563 4.243a2.21 2.21 0 0 0 2.167-.003l7.515-4.24a.73.73 0 0 0 .375-.62zm-16.236.563a.375.375 0 1 0-.368.654l6.359 3.587a2.59 2.59 0 0 0 2.551-.006l6.278-3.582a.375.375 0 0 0-.372-.652l-6.278 3.583c-.56.32-1.248.322-1.81.004z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"node-hardware-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M20.54 9.984v4.123a.69.69 0 0 1-.356.644l-7.13 4.024a2.1 2.1 0 0 1-2.057.003L3.82 14.752a.69.69 0 0 1-.355-.659V9.998a.69.69 0 0 1 .345-.653l7.156-4.172a2.1 2.1 0 0 1 2.117.003l7.112 4.17a.69.69 0 0 1 .344.638Z\"/><path d=\"M3.82 10.558a.699.699 0 0 1-.01-1.213l7.157-4.172a2.1 2.1 0 0 1 2.116.003l7.112 4.17a.699.699 0 0 1-.01 1.212l-7.132 4.023a2.1 2.1 0 0 1-2.056.003z\"/></g>"
|
||||
},
|
||||
"node-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"m13.5 7l3.5 3.5m-10 3l3.5 3.5m0-10L7 10.5m10 3L13.5 17\"/><circle cx=\"12\" cy=\"5.5\" r=\"2\"/><circle cx=\"12\" cy=\"18.5\" r=\"2\"/><circle cx=\"5.5\" cy=\"12\" r=\"2\"/><circle cx=\"18.5\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"5.5\" r=\".5\"/><circle cx=\"12\" cy=\"18.5\" r=\".5\"/><circle cx=\"5.5\" cy=\"12\" r=\".5\"/><circle cx=\"18.5\" cy=\"12\" r=\".5\"/></g>"
|
||||
},
|
||||
"pantheon-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M19.99 8H4.01A.01.01 0 0 1 4 7.99V6.507a.01.01 0 0 1 .007-.01l7.99-2.496h.006l7.99 2.497a.01.01 0 0 1 .007.01V7.99a.01.01 0 0 1-.01.01M4 20h16v-2H4zm2-3h2V9H6zm10 0h2V9h-2zm-5 0h2V9h-2z\"/>"
|
||||
},
|
||||
"pantheon-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" d=\"M19 7.5H5a.5.5 0 0 1-.5-.5v-.64a.5.5 0 0 1 .342-.474l7-2.333a.5.5 0 0 1 .316 0l7 2.333a.5.5 0 0 1 .342.474V7a.5.5 0 0 1-.5.5Z\"/><rect width=\"15\" height=\"2\" rx=\".5\" transform=\"matrix(1 0 0 -1 4.5 19.5)\"/><rect width=\"2\" height=\"6\" rx=\".5\" transform=\"matrix(1 0 0 -1 6 15.5)\"/><rect width=\"2\" height=\"6\" rx=\".5\" transform=\"matrix(1 0 0 -1 16 15.5)\"/><rect width=\"2\" height=\"6\" rx=\".5\" transform=\"matrix(1 0 0 -1 11 15.5)\"/></g>"
|
||||
},
|
||||
"password-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.91 8a.91.91 0 0 0-.91.91v6.18c0 .503.407.91.91.91h16.18a.91.91 0 0 0 .91-.91V8.91a.91.91 0 0 0-.91-.91zM7 13.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6.5-1.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m3.5 1.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"password-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"17\" height=\"9\" x=\"3.5\" y=\"7.5\" rx=\".5\"/><circle cx=\"7\" cy=\"12\" r=\"1.5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/><circle cx=\"17\" cy=\"12\" r=\"1.5\"/></g>"
|
||||
},
|
||||
"photo-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm3.5 6a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m-2.022 4.735l1.598-2.557a.5.5 0 0 1 .848 0l1.305 2.088l2.09-3.537a.5.5 0 0 1 .861 0l2.374 4.017a.5.5 0 0 1-.43.754h-4.748a.5.5 0 0 1-.139-.02a.5.5 0 0 1-.14.02H7.903a.5.5 0 0 1-.424-.765\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"photo-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"13\" height=\"13\" x=\"5.5\" y=\"5.5\" rx=\"1\"/><circle cx=\"9.5\" cy=\"9.5\" r=\"1\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m8 16l1.5-2.5L11 16l2.5-4l2.5 4\"/></g>"
|
||||
},
|
||||
"pie-chart-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M17.5 11.5a5 5 0 0 0-5-5v5z\"/><path fill=\"currentColor\" d=\"M11.5 7.5a5 5 0 1 0 5 5h-5z\"/>"
|
||||
},
|
||||
"pie-chart-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M17.5 11A4.5 4.5 0 0 0 13 6.5V11z\"/><path d=\"M11 8.5a4.5 4.5 0 1 0 4.5 4.5H11z\"/></g>"
|
||||
},
|
||||
"plus-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 3.25a.75.75 0 0 1 .75.75v7.25H20a.75.75 0 0 1 0 1.5h-7.25V20a.75.75 0 0 1-1.5 0v-7.25H4a.75.75 0 0 1 0-1.5h7.25V4a.75.75 0 0 1 .75-.75\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"plus-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M12 3.5v17m8.5-8.5h-17\"/>"
|
||||
},
|
||||
"podcast-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12 3a4 4 0 0 0-4 4h2a.5.5 0 0 1 0 1H8v1h2a.5.5 0 0 1 0 1H8v1h2a.5.5 0 0 1 0 1H8a4 4 0 0 0 8 0h-2a.5.5 0 0 1 0-1h2v-1h-2a.5.5 0 0 1 0-1h2V8h-2a.5.5 0 0 1 0-1h2a4 4 0 0 0-4-4\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M11.5 20v-2.5h1V20zm-3.5.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6.227 13.709a.5.5 0 0 1 .647.284a5.5 5.5 0 0 0 10.16.222a.5.5 0 0 1 .916.403a6.5 6.5 0 0 1-12.008-.262a.5.5 0 0 1 .285-.647\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"podcast-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"7\" height=\"12\" x=\"8.5\" y=\"3.5\" stroke-linejoin=\"round\" rx=\"3.5\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.5 11.5h2m3 0h2m-7-2h2m3 0h2m-7-2h2m3 0h2\"/><path d=\"M12 18v2.5\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.5 20.5h7\"/><path stroke-linecap=\"round\" d=\"M6.408 14.175a6 6 0 0 0 11.084.241\"/></g>"
|
||||
},
|
||||
"point-of-sale-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6 5.5A1.5 1.5 0 0 1 7.5 4h9A1.5 1.5 0 0 1 18 5.5v13a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 18.5zm2 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5V11a.5.5 0 0 1-.5.5h-7A.5.5 0 0 1 8 11zM8.5 17a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm5.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zM8 15.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m6.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm-3.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5M8.5 13a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm5.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2.5-.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"point-of-sale-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"11\" height=\"15\" x=\"6.5\" y=\"4.5\" rx=\"1.5\"/><path stroke-linejoin=\"round\" d=\"M8.5 6.5h7v5h-7z\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.5 17.5h1m5 0h1m-4 0h1m-4-2h1m5 0h1m-4 0h1m-4-2h1m5 0h1m-4 0h1\"/></g>"
|
||||
},
|
||||
"proxy-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 6.25a.75.75 0 0 1 .75.75v10a.75.75 0 1 1-1.5 0V7a.75.75 0 0 1 .75-.75M6.065 8.399a.75.75 0 0 1 1.06.02l2.953 3.06c.28.29.28.751 0 1.042l-2.953 3.06a.75.75 0 1 1-1.08-1.04l1.728-1.79H4a.75.75 0 1 1 0-1.5h3.773L6.046 9.458a.75.75 0 0 1 .019-1.06m10.461.001a.75.75 0 0 1 1.06.02l2.954 3.06c.28.29.28.751 0 1.042l-2.953 3.06a.75.75 0 1 1-1.08-1.04l1.727-1.79l-3.772-.001a.75.75 0 0 1 0-1.5h3.773l-1.728-1.79a.75.75 0 0 1 .02-1.061\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"proxy-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M12 17V7\"/><path stroke-linejoin=\"round\" d=\"M8.934 12H4m2.585 3.061L9.538 12L6.585 8.939M19.396 12h-4.934m2.585 3.061L20 12l-2.953-3.061\"/></g>"
|
||||
},
|
||||
"qr-code-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M5 5h4.5v4.5H5zm1.5 1.5V8H8V6.5zm8-1.5H19v4.5h-4.5zM16 6.5V8h1.5V6.5zm-11 8h4.5V19H5zM6.5 16v1.5H8V16z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M5 11.25h1.5v1.5H5zm3 0h1.5v1.5H8zm3.167 0h1.5v1.5h-1.5zm0 3.125h1.5v1.5h-1.5zm0 3.125h1.5V19h-1.5zm0-9.375h1.5v1.5h-1.5zm0-3.125h1.5v1.5h-1.5zm3.166 6.25h1.5v1.5h-1.5zm3.167 0H19v1.5h-1.5zm-3.167 3.125h1.5v1.5h-1.5zm3.167 0H19v1.5h-1.5zM14.333 17.5h1.5V19h-1.5zm3.167 0H19V19h-1.5z\"/>"
|
||||
},
|
||||
"qr-code-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linejoin=\"round\" d=\"M5.5 15H9v3.5H5.5zM15 5.5h3.5V9H15zm-9.5 0H9V9H5.5zm6.25 0h.5V6h-.5zm0 3.125h.5v.5h-.5zM8.625 11.75h.5v.5h-.5zm3.125 3.125h.5v.5h-.5zm0 3.125h.5v.5h-.5zM5.5 11.75H6v.5h-.5zm6.25 0h.5v.5h-.5zm3.125 0h.5v.5h-.5zm3.125 0h.5v.5H18zm-3.125 3.125h.5v.5h-.5zm3.125 0h.5v.5H18zM14.875 18h.5v.5h-.5zM18 18h.5v.5H18z\"/>"
|
||||
},
|
||||
"question-circle-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-8 3.5v2h-2v-2zM10.5 10c0-.844.59-1.5 1.5-1.5c.523 0 .88.17 1.105.395c.225.224.395.582.395 1.105c0 .32-.081.462-.168.57c-.113.14-.251.244-.507.437l-.183.14c-.335.256-.774.615-1.11 1.178c-.343.574-.532 1.279-.532 2.175h2c0-.604.123-.94.25-1.15c.133-.223.318-.394.608-.615q.053-.042.12-.09c.255-.191.626-.468.909-.817c.382-.472.613-1.06.613-1.828c0-.977-.33-1.87-.98-2.52S12.977 6.5 12 6.5c-2.09 0-3.5 1.627-3.5 3.5z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"question-circle-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path stroke-linecap=\"round\" d=\"M9 10c0-1.358 1.15-3 3-3s3 1.596 3 3c0 2.175-3 2.059-3 4.5m0 3v-1\"/></g>"
|
||||
},
|
||||
"question-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M10.75 15.75h2.5v2.5h-2.5z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 8c-1.195 0-2 1.086-2 2H8c0-1.802 1.496-4 4-4c2.496 0 4 2.142 4 4c0 1.578-1.108 2.378-1.794 2.873l-.116.084c-.755.552-1.09.866-1.09 1.543h-2c0-1.762 1.161-2.61 1.907-3.155l.003-.002c.832-.609 1.09-.84 1.09-1.343c0-.95-.796-2-2-2\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"question-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M9 10c0-1.358 1.15-3 3-3s3 1.596 3 3c0 2.175-3 2.059-3 4.5m0 3v-1\"/>"
|
||||
},
|
||||
"receive-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M5 14.997a.75.75 0 0 1 .75.75V18c0 .138.112.25.25.25h12a.25.25 0 0 0 .25-.25v-2.253a.75.75 0 0 1 1.5 0V18A1.75 1.75 0 0 1 18 19.75H6A1.75 1.75 0 0 1 4.25 18v-2.253a.75.75 0 0 1 .75-.75M12.202 4.25a.75.75 0 0 1 .75.75v8.086a.75.75 0 0 1-1.5 0V5a.75.75 0 0 1 .75-.75\"/><path d=\"M8.322 10.626a.75.75 0 0 1 1.06-.013l2.82 2.755l2.82-2.755a.75.75 0 1 1 1.048 1.073l-3.344 3.267a.75.75 0 0 1-1.048 0l-3.344-3.267a.75.75 0 0 1-.012-1.06\"/></g>"
|
||||
},
|
||||
"receive-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 15.747V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.253M12.202 13.5V5m3.344 6.15l-3.344 3.266l-3.344-3.266\"/>"
|
||||
},
|
||||
"refresh-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M6.64 9.788a.75.75 0 0 1 .53.918a5 5 0 0 0 7.33 5.624a.75.75 0 1 1 .75 1.3a6.501 6.501 0 0 1-9.529-7.312a.75.75 0 0 1 .919-.53M8.75 6.37a6.5 6.5 0 0 1 9.529 7.312a.75.75 0 1 1-1.45-.388A5.001 5.001 0 0 0 9.5 7.67a.75.75 0 1 1-.75-1.3\"/><path d=\"M5.72 9.47a.75.75 0 0 1 1.06 0l2.5 2.5a.75.75 0 1 1-1.06 1.06l-1.97-1.97l-1.97 1.97a.75.75 0 0 1-1.06-1.06zm9 1.5a.75.75 0 0 1 1.06 0l1.97 1.97l1.97-1.97a.75.75 0 1 1 1.06 1.06l-2.5 2.5a.75.75 0 0 1-1.06 0l-2.5-2.5a.75.75 0 0 1 0-1.06\"/></g>"
|
||||
},
|
||||
"refresh-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M6.446 10.512a5.75 5.75 0 0 0 8.429 6.468m2.679-3.492A5.75 5.75 0 0 0 9.125 7.02\"/><path stroke-linejoin=\"round\" d=\"m3.75 12.5l2.5-2.5l2.5 2.5m6.5-1l2.5 2.5l2.5-2.5\"/></g>"
|
||||
},
|
||||
"relay-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M13.383 4.844a.75.75 0 0 1 .935-.5a8 8 0 0 1-3.765 15.524a.75.75 0 0 1 .271-1.475a6.5 6.5 0 0 0 3.06-12.614a.75.75 0 0 1-.501-.935m-2.66-.02a.75.75 0 0 1-.514.928a6.5 6.5 0 0 0-2.544 11.091a.75.75 0 0 1-1 1.118A8 8 0 0 1 9.794 4.31a.75.75 0 0 1 .928.514\"/><path d=\"M10.842 6.88a5.25 5.25 0 0 1 6.276 6.292a.75.75 0 1 1-1.463-.335a3.75 3.75 0 1 0-2.041 2.548a.75.75 0 0 1 .645 1.354a5.25 5.25 0 1 1-3.417-9.86\"/><path d=\"M12.297 11.045a1 1 0 0 0-.986.23a.75.75 0 0 1-1.033-1.087a2.5 2.5 0 1 1 .875 4.164a.75.75 0 1 1 .508-1.411a1 1 0 1 0 .636-1.896\"/></g>"
|
||||
},
|
||||
"relay-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M10.689 19.13A7.25 7.25 0 0 0 14.1 5.06m-4.098-.03a7.25 7.25 0 0 0-2.837 12.372\"/><path d=\"M16.387 13.004a4.5 4.5 0 1 0-2.45 3.058\"/><path d=\"M10.794 10.732a1.75 1.75 0 1 1 .613 2.914\"/></g>"
|
||||
},
|
||||
"rocket-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 16a.5.5 0 0 1 0 .707l-1 1A.5.5 0 1 1 6.293 17l1-1A.5.5 0 0 1 8 16m1.5 1.5a.5.5 0 0 1 0 .707l-1 1a.5.5 0 1 1-.707-.707l1-1a.5.5 0 0 1 .707 0m-3-3a.5.5 0 0 1 0 .707l-1 1a.5.5 0 1 1-.707-.708l1-1a.5.5 0 0 1 .707 0m6.102-8.483a11.54 11.54 0 0 0-5.797 6.495l4.684 4.685a11.54 11.54 0 0 0 6.495-5.798zm1.045 5.337a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M18.41 10.41a11.5 11.5 0 0 0 .737-4.057q-.001-.718-.086-1.414a12 12 0 0 0-1.414-.086c-1.428 0-2.795.26-4.057.736zm-6.144 7.562l1.027 1.027a.5.5 0 0 0 .854-.353v-1.614a12.5 12.5 0 0 1-1.881.94m-6.238-6.238q.393-.985.94-1.881H5.354a.5.5 0 0 0-.354.854z\"/>"
|
||||
},
|
||||
"rocket-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M11.449 16.449L7.55 12.55C8.998 8.167 13.13 5 18 5q.485 0 .959.041q.04.474.041.96c0 4.87-3.165 9-7.552 10.448Z\"/><path stroke-linecap=\"round\" d=\"m8 16l-1 1m2.5.5l-1 1m-2-4l-1 1\"/><circle cx=\"13\" cy=\"11\" r=\"1\"/><path stroke-linecap=\"round\" d=\"m14 6l4 4\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 15.5V19l-9-9h3.5\"/></g>"
|
||||
},
|
||||
"safe-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M7.5 7.5v9h9v-9zM12 14c.37 0 .718-.101 1.016-.277l1.13 1.13a.5.5 0 0 0 .708-.707l-1.13-1.13a2 2 0 0 0-.001-2.032l1.13-1.13a.5.5 0 0 0-.707-.708l-1.13 1.13A2 2 0 0 0 12 10c-.37 0-.718.101-1.016.277l-1.13-1.13a.5.5 0 1 0-.708.707l1.13 1.13A2 2 0 0 0 10 12c0 .37.101.718.277 1.016l-1.13 1.13a.5.5 0 0 0 .707.708l1.13-1.13A2 2 0 0 0 12 14\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M5 6a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v11.935a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1zm1.5 1a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5z\" clip-rule=\"evenodd\"/><circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"currentColor\"/><path fill=\"currentColor\" d=\"M7 19h2v.578a.42.42 0 0 1-.422.422H7.422A.42.42 0 0 1 7 19.578zm8 0h2v.578a.42.42 0 0 1-.422.422h-1.156a.42.42 0 0 1-.422-.422z\"/>"
|
||||
},
|
||||
"safe-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"13\" height=\"13\" x=\"5.5\" y=\"5.5\" rx=\"1\"/><rect width=\"9\" height=\"9\" x=\"7.5\" y=\"7.5\" rx=\".5\"/><path stroke-linecap=\"square\" d=\"M8.5 19.5h-1m9 0h-1\"/><circle cx=\"12\" cy=\"12\" r=\"1.25\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.5 9.5L11 11m-1.5 3.5L11 13m2 0l1.5 1.5M13 11l1.5-1.5\"/></g>"
|
||||
},
|
||||
"satoshi-v1-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0M8.693 8.742l7.637 2.063l.337-1.462L9.029 7.28zm5.526-3.05l-.406 1.774l-1.448-.392l.407-1.774zM11.227 18.7l.408-1.774l-1.448-.391l-.408 1.774zm4.421-4.934L8.011 11.7l.336-1.462l7.637 2.066zm-8.316.89l7.638 2.064l.336-1.462l-7.638-2.064z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"satoshi-v1-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M12 20.5a8.5 8.5 0 1 0 0-17a8.5 8.5 0 0 0 0 17ZM8.86 8.011l7.639 2.063m-3.41-2.804l.406-1.774m-2.992 13.008l.408-1.773M8.18 10.969l7.636 2.066m-8.316.89l7.638 2.064\"/>"
|
||||
},
|
||||
"satoshi-v2-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12.75 18.5V21h-1.5v-2.5zM17 16.75H7v-1.5h10zm0-4H7v-1.5h10zm0-4H7v-1.5h10zM12.75 3v2.5h-1.5V3z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"satoshi-v2-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M7 7.91h10m-5-2.455V3m0 18v-2.455M7 12h10M7 16.09h10\"/>"
|
||||
},
|
||||
"satoshi-v3-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M13.96 5.54a6.8 6.8 0 0 0-1.961-.29c-2.207 0-3.652.809-4.806 2.106c-.967 1.087-1.338 2.438-1.338 3.449c0 2.902 2.813 5.013 5.953 4.945h.022c.602 0 1.183-.2 1.599-.502c.424-.309.593-.651.593-.92c0-.541-.208-.842-.557-1.084c-.407-.283-.987-.468-1.71-.672l-.172-.048c-.626-.175-1.36-.38-1.937-.72a2.6 2.6 0 0 1-.885-.812a2.3 2.3 0 0 1-.364-1.28c0-1.775 1.64-2.962 3.433-2.962c1.766 0 3.186 1.04 3.466 2.268a.75.75 0 0 1-1.463.333c-.083-.367-.736-1.101-2.003-1.101c-1.24 0-1.933.767-1.933 1.461a.8.8 0 0 0 .12.46c.072.11.193.225.389.34c.4.236.948.39 1.637.584l.12.034c.684.193 1.51.433 2.158.883c.706.49 1.201 1.23 1.201 2.315c0 .906-.544 1.65-1.212 2.134a4.3 4.3 0 0 1-2.478.789c-3.676.075-7.477-2.43-7.477-6.445c0-1.331.476-3.05 1.717-4.446C7.49 4.766 9.332 3.75 12 3.75m0 0a8.25 8.25 0 1 1-2.134 16.219a.75.75 0 0 1 .388-1.449A6.75 6.75 0 0 0 13.96 5.54\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"satoshi-v3-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.037 19.26a7.43 7.43 0 0 0 4.578-.22a7.5 7.5 0 0 0 3.6-2.86a7.6 7.6 0 0 0 1.259-4.443a7.56 7.56 0 0 0-1.54-4.308a7.55 7.55 0 0 0-3.787-2.622a7.5 7.5 0 0 0-2.185-.323c-2.433 0-4.086.92-5.37 2.362c-1.11 1.249-1.54 2.783-1.54 3.965c0 3.462 3.315 5.704 6.736 5.704c1.527 0 2.948-1 2.948-2.179c0-3.26-5.632-1.813-5.632-4.628c0-1.234 1.173-2.222 2.687-2.222s2.56.9 2.732 1.686\"/>"
|
||||
},
|
||||
"scan-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M18.25 14a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1 0-1.5h2.75v-2.75a.75.75 0 0 1 .75-.75m-12.5 0a.75.75 0 0 1 .75.75v2.75h2.75a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75v-3.5a.75.75 0 0 1 .75-.75M14 5.75a.75.75 0 0 1 .75-.75h3.496a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V6.5H14.75a.75.75 0 0 1-.75-.75m-9 0A.75.75 0 0 1 5.75 5h3.5a.75.75 0 0 1 0 1.5H6.5v2.75a.75.75 0 0 1-1.5 0z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"scan-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M18.5 15v3.5H15m-6 0H5.5V15M15 5.5h3.496V9M9 5.5H5.5V9\"/>"
|
||||
},
|
||||
"sd-card-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m6 9l5-5h5.5A1.5 1.5 0 0 1 18 5.5v13a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 18.5zm9.75-3a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5a.75.75 0 0 0-.75-.75M13 6.75a.75.75 0 0 1 1.5 0v2.5a.75.75 0 0 1-1.5 0zM11.75 6a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5a.75.75 0 0 0-.75-.75\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"sd-card-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"m6.5 8.5l4-4H16A1.5 1.5 0 0 1 17.5 6v12a1.5 1.5 0 0 1-1.5 1.5H8A1.5 1.5 0 0 1 6.5 18z\"/><path stroke-linecap=\"round\" d=\"M11.5 7v3m2-3v3m2-3v3\"/></g>"
|
||||
},
|
||||
"search-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M10.5 5.5a5 5 0 1 0 0 10a5 5 0 0 0 0-10m-6.5 5a6.5 6.5 0 1 1 13 0a6.5 6.5 0 0 1-13 0\"/><path d=\"M14.47 14.47a.75.75 0 0 1 1.06 0l4 4a.75.75 0 1 1-1.06 1.06l-4-4a.75.75 0 0 1 0-1.06\"/></g>"
|
||||
},
|
||||
"search-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"11\" cy=\"11\" r=\"5.5\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m15 15l4 4\"/></g>"
|
||||
},
|
||||
"send-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M5 14.997a.75.75 0 0 1 .75.75V18c0 .138.112.25.25.25h12a.25.25 0 0 0 .25-.25v-2.253a.75.75 0 0 1 1.5 0V18A1.75 1.75 0 0 1 18 19.75H6A1.75 1.75 0 0 1 4.25 18v-2.253a.75.75 0 0 1 .75-.75\"/><path d=\"M12.202 5.58a.75.75 0 0 1 .75.75v8.086a.75.75 0 0 1-1.5 0V6.331a.75.75 0 0 1 .75-.75\"/><path d=\"M11.678 4.464a.75.75 0 0 1 1.048 0L16.07 7.73a.75.75 0 0 1-1.048 1.073l-2.82-2.754l-2.82 2.754A.75.75 0 1 1 8.334 7.73z\"/></g>"
|
||||
},
|
||||
"send-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 15.747V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.253m-6.798-9.83v8.5m3.344-6.15L12.202 5L8.858 8.267\"/>"
|
||||
},
|
||||
"share-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M20.336 3.221L3.873 8.71a.35.35 0 0 0-.027.654l6.05 2.593a.2.2 0 0 0 .196-.021l5.931-4.238c.184-.13.41.096.28.28l-4.238 5.931a.2.2 0 0 0-.02.195l2.592 6.05a.35.35 0 0 0 .654-.026L20.78 3.664a.35.35 0 0 0-.443-.443\"/>"
|
||||
},
|
||||
"share-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M20.5 3.5L3.5 9l6.5 3l7-5l-5 7l3 6.5z\"/>"
|
||||
},
|
||||
"shared-wallet-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M5 4.5A1.5 1.5 0 0 1 6.5 3h8A1.5 1.5 0 0 1 16 4.5v4.504h-1V4.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5H11v1H6.5A1.5 1.5 0 0 1 5 16.5z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M6 15h5v2H6z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 11.5a1.5 1.5 0 0 1 1.5-1.5h4a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-4a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5z\" clip-rule=\"evenodd\"/><path fill=\"currentColor\" d=\"M12.5 18.5h6v1a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z\"/>"
|
||||
},
|
||||
"shared-wallet-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"square\" stroke-linejoin=\"round\" d=\"M15.5 8.504V4.5a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h4\"/><rect width=\"6\" height=\"10\" x=\"12.5\" y=\"10.5\" rx=\"1\"/><path stroke-linecap=\"square\" d=\"M13.5 18h4m-11-3h4\"/></g>"
|
||||
},
|
||||
"shield-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M5 7c0-.276.225-.499.498-.535c2.149-.28 5.282-2.186 6.224-2.785a.52.52 0 0 1 .556 0c.942.599 4.075 2.504 6.224 2.785c.273.036.498.259.498.535v4.75c0 6.5-7 8.75-7 8.75s-7-2.25-7-8.75z\"/>"
|
||||
},
|
||||
"shield-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linejoin=\"round\" d=\"M5.5 7c2 0 6.5-3 6.5-3s4.5 3 6.5 3v4.5C18.5 18 12 20 12 20s-6.5-2-6.5-8.5z\"/>"
|
||||
},
|
||||
"sign-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M6 6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2zm3.49 11.598l.001-.005l.004-.02a12 12 0 0 1 .078-.35c.053-.228.129-.53.219-.83c.07-.234.146-.455.222-.633c.094.189.193.424.3.681l.03.074c.117.283.243.587.366.825c.067.128.147.265.24.375c.074.087.26.285.55.285c.326 0 .54-.196.658-.337c.106-.128.193-.287.253-.397l.014-.024l.074-.131q.031.052.073.13l.013.023c.06.11.148.27.255.4c.12.142.334.336.66.336c.256 0 .507-.13.67-.224c.189-.11.383-.25.551-.382a10 10 0 0 0 .57-.482l.047-.044l.004-.003a.5.5 0 0 0-.684-.73l-.002.002l-.008.008l-.032.029a9 9 0 0 1-.51.432a4 4 0 0 1-.44.306q-.06.034-.099.053a4 4 0 0 1-.118-.206l-.006-.01a2.4 2.4 0 0 0-.275-.42A.88.88 0 0 0 12.5 16c-.32 0-.539.18-.668.327c-.12.138-.214.307-.278.422l-.01.02c-.087-.18-.18-.403-.281-.649l-.025-.061a9 9 0 0 0-.417-.911a2 2 0 0 0-.266-.383c-.094-.1-.282-.265-.555-.265c-.276 0-.464.168-.558.273c-.105.117-.189.26-.256.394a6 6 0 0 0-.352.94a15 15 0 0 0-.323 1.286v.006l-.001.002a.5.5 0 0 0 .98.197M9 6.54a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.71a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1H9a.5.5 0 0 1-.5-.5M9 11a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"sign-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 16.5s.501-2.5 1-2.5s1.07 2.5 1.5 2.5s.535-1 1-1s.564 1 1 1s1.5-1 1.5-1M9 7.04h6M9 9.25h6M9 11.5h6\"/><rect width=\"11\" height=\"15\" x=\"6.5\" y=\"4.5\" rx=\"1.5\"/></g>"
|
||||
},
|
||||
"snowflake-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 3.25a.75.75 0 0 1 .75.75v3.175l2.22-2.22a.75.75 0 1 1 1.06 1.06l-3.28 3.281v1.954h1.925l3.295-3.295a.75.75 0 1 1 1.06 1.06l-2.234 2.235H20a.75.75 0 0 1 0 1.5h-3.175l2.205 2.205a.75.75 0 1 1-1.06 1.06l-3.266-3.265H12.75v1.925l3.28 3.28a.75.75 0 1 1-1.06 1.06l-2.22-2.219V20a.75.75 0 0 1-1.5 0v-3.204l-2.22 2.22a.75.75 0 0 1-1.06-1.06l3.28-3.281V12.75H9.296L6.03 16.016a.75.75 0 0 1-1.06-1.06l2.205-2.206H4a.75.75 0 0 1 0-1.5h3.204L4.97 9.016a.75.75 0 0 1 1.06-1.06l3.295 3.294h1.925V9.296l-3.28-3.28a.75.75 0 1 1 1.06-1.06l2.22 2.219V4a.75.75 0 0 1 .75-.75\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"snowflake-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4v16m8-8H4m14.5-3.514l-3.5 3.5l3.5 3.5m-13 0l3.5-3.5l-3.5-3.5m10 10l-3.5-3.5l-3.5 3.5m0-13l3.5 3.5l3.5-3.5\"/>"
|
||||
},
|
||||
"sofa-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M3 11.5a1.5 1.5 0 0 1 3 0V14h12v-2.5a1.5 1.5 0 0 1 3 0v5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 16.5z\"/><path fill=\"currentColor\" d=\"M5.5 7.5A1.5 1.5 0 0 1 7 6h10a1.5 1.5 0 0 1 1.5 1.5v1.708A2.5 2.5 0 0 0 17 11.5V13H7v-1.5a2.5 2.5 0 0 0-1.5-2.292z\"/>"
|
||||
},
|
||||
"sofa-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M3.5 11.5a1 1 0 1 1 2 0v3h13v-3a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1z\"/><path d=\"M6 8a1.5 1.5 0 0 1 1.5-1.5h9A1.5 1.5 0 0 1 18 8v.708A2.5 2.5 0 0 0 16.5 11v1.5h-9V11A2.5 2.5 0 0 0 6 8.708z\"/></g>"
|
||||
},
|
||||
"star-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M11.334 3.549c.21-.645 1.122-.645 1.332 0L14.2 8.272a.7.7 0 0 0 .666.483h4.966c.678 0 .96.868.411 1.267l-4.017 2.918a.7.7 0 0 0-.254.783l1.534 4.723c.21.645-.529 1.18-1.077.782l-4.017-2.918a.7.7 0 0 0-.823 0L7.57 19.228c-.548.399-1.287-.137-1.077-.782l1.534-4.723a.7.7 0 0 0-.254-.783l-4.017-2.918c-.549-.399-.267-1.267.411-1.267h4.966a.7.7 0 0 0 .666-.483z\"/>"
|
||||
},
|
||||
"star-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M11.762 3.732a.25.25 0 0 1 .476 0l1.726 5.314a.25.25 0 0 0 .238.173h5.588a.25.25 0 0 1 .147.452l-4.52 3.285a.25.25 0 0 0-.091.28l1.726 5.313a.25.25 0 0 1-.384.28l-4.521-3.285a.25.25 0 0 0-.294 0l-4.52 3.285a.25.25 0 0 1-.385-.28l1.726-5.314a.25.25 0 0 0-.09-.28L4.063 9.672a.25.25 0 0 1 .147-.452h5.588a.25.25 0 0 0 .238-.173z\"/>"
|
||||
},
|
||||
"sun-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12 16.5A4.505 4.505 0 0 1 7.5 12c0-2.481 2.019-4.5 4.5-4.5s4.5 2.019 4.5 4.5s-2.019 4.5-4.5 4.5\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 3a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 12 3m6 9a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5M3 12a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2A.5.5 0 0 1 3 12m9 6a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2a.5.5 0 0 1 .5-.5m6.354-12.354a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708-.708l1.5-1.5a.5.5 0 0 1 .708 0m-10.5 10.5a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708-.708l1.5-1.5a.5.5 0 0 1 .708 0m-2.208-10.5a.5.5 0 0 1 .708 0l1.5 1.5a.5.5 0 1 1-.708.708l-1.5-1.5a.5.5 0 0 1 0-.708m10.5 10.5a.5.5 0 0 1 .708 0l1.5 1.5a.5.5 0 0 1-.708.708l-1.5-1.5a.5.5 0 0 1 0-.708\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"sun-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M12 16c-2.206 0-4-1.794-4-4s1.794-4 4-4s4 1.794 4 4s-1.794 4-4 4Z\"/><path stroke-linecap=\"round\" d=\"M12 3.5v2m8.5 6.5h-2m-13 0h-2m8.5 6.5v2m4.5-13L18 6M6 18l1.5-1.5M6 6l1.5 1.5m9 9L18 18\"/></g>"
|
||||
},
|
||||
"tag-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m10.647 18.646l-5.293-5.292a.5.5 0 0 1 0-.708l6.5-6.5A.5.5 0 0 1 12.207 6h3.586l.069.005Q15.929 6 16 6a2 2 0 0 1 1.995 2.139l.005.068v3.586a.5.5 0 0 1-.146.353l-6.5 6.5a.5.5 0 0 1-.707 0M12 9.672a.5.5 0 0 0-1 0v5.656a.5.5 0 1 0 1 0zm-1.914 2.12a1 1 0 1 1-1.415 1.415a1 1 0 0 1 1.415-1.414m4.243 1.415a1 1 0 1 0-1.415-1.414a1 1 0 0 0 1.415 1.414\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"tag-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"m5.927 13.177l4.896 4.896a.25.25 0 0 0 .354 0l6.25-6.25a.25.25 0 0 0 .073-.177V8.104l-.002-.03L17.5 8a1.5 1.5 0 0 0-1.574-1.498l-.03-.002h-3.543a.25.25 0 0 0-.176.073l-6.25 6.25a.25.25 0 0 0 0 .354Z\"/><path stroke-linecap=\"round\" d=\"M11.5 15v-4.5\"/><path d=\"M9.854 12.396a.5.5 0 1 1-.708.708a.5.5 0 0 1 .708-.708Zm4 0a.5.5 0 1 1-.708.708a.5.5 0 0 1 .708-.708Z\"/></g>"
|
||||
},
|
||||
"tip-jar-filled": {
|
||||
"body": "<rect width=\"13\" height=\"8\" x=\"5.5\" y=\"13\" fill=\"currentColor\" rx=\"1\"/><path fill=\"currentColor\" d=\"M6.5 11.5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v1h-11z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M12 10a3.5 3.5 0 1 0 0-7a3.5 3.5 0 0 0 0 7m.727-4.35a.5.5 0 0 0-.97-.241l-.484 1.94a.5.5 0 1 0 .97.242z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"tip-jar-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"12\" height=\"7.5\" x=\"6\" y=\"13\" rx=\"1\"/><path d=\"M7 12a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v1H7z\"/><circle cx=\"12\" cy=\"6.5\" r=\"3\"/><path stroke-linecap=\"round\" d=\"m12.242 5.53l-.484 1.94\"/></g>"
|
||||
},
|
||||
"transactions-filled": {
|
||||
"body": "<circle cx=\"5.5\" cy=\"7.5\" r=\"1.5\" fill=\"currentColor\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 6.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/><circle cx=\"5.5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 11a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8A.5.5 0 0 1 8 11m0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 8 13\" clip-rule=\"evenodd\"/><circle cx=\"5.5\" cy=\"16.5\" r=\"1.5\" fill=\"currentColor\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 15.5a.5.5 0 0 1 .5-.5H18a.5.5 0 0 1 0 1H8.5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"transactions-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><circle cx=\"5.5\" cy=\"7.5\" r=\"1\"/><path stroke-linecap=\"round\" d=\"M8.5 6.5h11m-11 2h6\"/><circle cx=\"5.5\" cy=\"12\" r=\"1\"/><path stroke-linecap=\"round\" d=\"M8.5 11h8m-8 2h7\"/><circle cx=\"5.5\" cy=\"16.5\" r=\"1\"/><path stroke-linecap=\"round\" d=\"M8.5 15.5H18m-9.5 2h4\"/></g>"
|
||||
},
|
||||
"transfer-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M4 15.243a.75.75 0 0 1 .75.75V19c0 .138.112.25.25.25h14a.25.25 0 0 0 .25-.25v-3.007a.75.75 0 0 1 1.5 0V19A1.75 1.75 0 0 1 19 20.75H5A1.75 1.75 0 0 1 3.25 19v-3.007a.75.75 0 0 1 .75-.75\"/><path d=\"M19.87 10.893c.3.286.311.76.025 1.06l-3.047 3.199a.75.75 0 0 1-1.086 0l-3.048-3.198a.75.75 0 1 1 1.086-1.035l2.505 2.628l2.504-2.628a.75.75 0 0 1 1.06-.026\"/><path d=\"M11.352 4.75A4.2 4.2 0 0 0 7.15 8.952v5.006a.75.75 0 0 1-1.5 0V8.952a5.702 5.702 0 0 1 11.405 0v5.006a.75.75 0 0 1-1.5 0V8.952a4.2 4.2 0 0 0-4.203-4.202\"/></g>"
|
||||
},
|
||||
"transfer-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 15.993V19a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3.007m-.648-4.557l-3.047 3.198l-3.048-3.198\"/><path d=\"M6.4 13.958V8.952A4.95 4.95 0 0 1 11.352 4v0a4.95 4.95 0 0 1 4.953 4.952v5.006\"/></g>"
|
||||
},
|
||||
"trash-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M5.25 6.91A.75.75 0 0 1 6 6.16h12a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1-.75-.75\"/><path d=\"M11.333 4.75c-.69 0-1.25.56-1.25 1.25v.91h-1.5V6a2.75 2.75 0 0 1 2.75-2.75h1.334A2.75 2.75 0 0 1 15.417 6v.91h-1.5V6c0-.69-.56-1.25-1.25-1.25zM6 6.91L8 20h8l2-13.09zm6.5 3.636a.5.5 0 1 0-1 0v5.818a.5.5 0 1 0 1 0zm-3.224-.5a.476.476 0 0 1 .55.423l.666 5.818a.525.525 0 0 1-.435.576a.477.477 0 0 1-.549-.423l-.667-5.818a.525.525 0 0 1 .435-.575m5.883.576a.525.525 0 0 0-.435-.575a.476.476 0 0 0-.55.422l-.666 5.818a.525.525 0 0 0 .435.576a.476.476 0 0 0 .549-.423z\"/></g>"
|
||||
},
|
||||
"trash-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M6.6 6.91L8.4 20h7.2l1.8-13.09\"/><path stroke-linecap=\"round\" d=\"M6 6.667h12\"/><path d=\"M14.571 7V6a2 2 0 0 0-2-2H11.43a2 2 0 0 0-2 2v1\"/><path stroke-linecap=\"round\" d=\"M11.98 10.546v5.819m-2.38-5.82l.6 5.82m4.2-5.819l-.6 5.819\"/></g>"
|
||||
},
|
||||
"two-keys-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M20.854 10.308c.6 2.411-.923 4.867-3.403 5.486q-.466.115-.928.137l-2.666 4.298a2 2 0 0 1-1.216.887l-1.236.308a1 1 0 0 1-1.212-.729l-.28-1.12a2 2 0 0 1 .241-1.538l2.445-3.943a4.4 4.4 0 0 1-.728-1.547c-.6-2.411.922-4.867 3.403-5.486s4.978.835 5.58 3.247m-3.872 1.48c.552-.137.89-.683.756-1.219c-.133-.536-.688-.859-1.24-.721c-.55.137-.89.683-.756 1.219s.69.859 1.24.721\"/><path d=\"M11.251 12.187c2.48-.619 4.004-3.075 3.403-5.486c-.601-2.412-3.1-3.865-5.58-3.247S5.07 6.529 5.672 8.94a4.4 4.4 0 0 0 .728 1.547l-2.445 3.942a2 2 0 0 0-.241 1.538l.28 1.121a1 1 0 0 0 1.211.728l1.236-.308a2 2 0 0 0 1.216-.886l2.666-4.298q.462-.022.928-.137m.288-5.225c.133.536-.205 1.082-.756 1.219s-1.106-.186-1.24-.721c-.134-.536.205-1.082.756-1.22s1.106.186 1.24.722\"/></g>"
|
||||
},
|
||||
"two-keys-outline": {
|
||||
"body": "<g fill=\"none\"><g stroke=\"currentColor\" clip-path=\"url(#bitcoinIconsTwoKeysOutline0)\"><path stroke-linecap=\"round\" d=\"M14.257 5.976c-.85-1.7-2.832-2.638-4.802-2.147c-2.27.566-3.663 2.815-3.112 5.023c.157.633.458 1.194.86 1.659l-2.709 4.37a1.5 1.5 0 0 0-.18 1.153l.36 1.44a.25.25 0 0 0 .302.183l1.528-.381a1.5 1.5 0 0 0 .912-.666l2.884-4.655\"/><ellipse cx=\"10.82\" cy=\"7.266\" rx=\"1.059\" ry=\"1.03\" transform=\"rotate(-14 10.82 7.266)\"/><path d=\"M17.81 15.61c2.27-.566 3.664-2.815 3.113-5.023c-.55-2.209-2.837-3.54-5.106-2.974c-2.27.566-3.663 2.815-3.113 5.023c.158.633.458 1.194.86 1.659l-2.708 4.37a1.5 1.5 0 0 0-.18 1.153l.359 1.44a.25.25 0 0 0 .303.183l1.527-.381a1.5 1.5 0 0 0 .912-.666l2.885-4.655q.569.014 1.149-.129Z\"/><ellipse cx=\"17.203\" cy=\"10.984\" rx=\"1.059\" ry=\"1.03\" transform=\"rotate(-14 17.203 10.984)\"/></g><defs><clipPath id=\"bitcoinIconsTwoKeysOutline0\"><path fill=\"#fff\" d=\"M0 0h24v24H0z\"/></clipPath></defs></g>"
|
||||
},
|
||||
"unlock-filled": {
|
||||
"body": "<rect width=\"14\" height=\"10\" x=\"5\" y=\"11.225\" fill=\"currentColor\" rx=\"2\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M10 5.75a.75.75 0 0 0-.75.75V12a.75.75 0 0 1-1.5 0V6.5A2.25 2.25 0 0 1 10 4.25h4a2.25 2.25 0 0 1 2.25 2.25v2.522a.75.75 0 0 1-1.5 0V6.5a.75.75 0 0 0-.75-.75z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"unlock-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><rect width=\"14\" height=\"10\" x=\"5\" y=\"10.989\" rx=\"2\"/><path stroke-linecap=\"round\" d=\"m15.5 8l-.008-1.742a1.5 1.5 0 0 0-1.5-1.494h-3.978a1.5 1.5 0 0 0-1.5 1.5v4.73\"/></g>"
|
||||
},
|
||||
"unmixed-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M15.08 2.462a.75.75 0 0 1 1.06.016l3.398 3.5a.75.75 0 0 1 0 1.045l-3.398 3.5a.75.75 0 1 1-1.077-1.045l2.163-2.228H5a.75.75 0 0 1 0-1.5h12.226l-2.162-2.227a.75.75 0 0 1 .015-1.061m0 11a.75.75 0 0 1 1.06.015l3.398 3.5a.75.75 0 0 1 0 1.045l-3.398 3.5a.75.75 0 1 1-1.077-1.045l2.163-2.227H5a.75.75 0 0 1 0-1.5h12.227l-2.164-2.228a.75.75 0 0 1 .016-1.06\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"unmixed-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M18 17.5H5M15.602 21L19 17.5L15.602 14M18 6.5H5M15.602 10L19 6.5L15.602 3\"/>"
|
||||
},
|
||||
"usb-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M7.5 7h9v9.5a4.5 4.5 0 1 1-9 0z\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M9 3h6v5H9zm1 1v3h4V4z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"usb-outline": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" d=\"M8 7.5h8v9a4 4 0 0 1-8 0zm1.5-4h5v4h-5z\"/>"
|
||||
},
|
||||
"verify-filled": {
|
||||
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M15.418 5.643a1.25 1.25 0 0 0-1.34-.555l-1.798.413a1.25 1.25 0 0 1-.56 0l-1.798-.413a1.25 1.25 0 0 0-1.34.555l-.98 1.564c-.1.16-.235.295-.395.396l-1.564.98a1.25 1.25 0 0 0-.555 1.338l.413 1.8a1.25 1.25 0 0 1 0 .559l-.413 1.799a1.25 1.25 0 0 0 .555 1.339l1.564.98c.16.1.295.235.396.395l.98 1.564c.282.451.82.674 1.339.555l1.798-.413a1.25 1.25 0 0 1 .56 0l1.799.413a1.25 1.25 0 0 0 1.339-.555l.98-1.564c.1-.16.235-.295.395-.395l1.565-.98a1.25 1.25 0 0 0 .554-1.34L18.5 12.28a1.25 1.25 0 0 1 0-.56l.413-1.799a1.25 1.25 0 0 0-.554-1.339l-1.565-.98a1.25 1.25 0 0 1-.395-.395zm-.503 4.127a.5.5 0 0 0-.86-.509l-2.615 4.426l-1.579-1.512a.5.5 0 1 0-.691.722l2.034 1.949a.5.5 0 0 0 .776-.107z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"verify-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M14.049 5.54a1 1 0 0 1 1.071.443l.994 1.587a1 1 0 0 0 .316.316l1.587.994a1 1 0 0 1 .444 1.072l-.42 1.824a1 1 0 0 0 0 .448l.42 1.825a1 1 0 0 1-.444 1.07l-1.587.995a1 1 0 0 0-.316.316l-.994 1.587a1 1 0 0 1-1.071.444l-1.825-.42a1 1 0 0 0-.447 0l-1.825.42a1 1 0 0 1-1.071-.444l-.994-1.587a1 1 0 0 0-.317-.316l-1.586-.994a1 1 0 0 1-.444-1.071l.419-1.825a1 1 0 0 0 0-.448l-.42-1.824a1 1 0 0 1 .445-1.072l1.586-.994a1 1 0 0 0 .317-.316l.994-1.587a1 1 0 0 1 1.07-.443l1.826.419a1 1 0 0 0 .447 0z\"/><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m9.515 12.536l2.035 1.949l2.935-4.97\"/></g>"
|
||||
},
|
||||
"visible-filled": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12 14a2 2 0 1 0 0-4a2 2 0 0 0 0 4\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M21 12c0 2.761-4.03 5-9 5s-9-2.239-9-5s4.03-5 9-5s9 2.239 9 5m-5 0a4 4 0 1 1-8 0a4 4 0 0 1 8 0\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"visible-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><ellipse cx=\"12\" cy=\"12\" rx=\"8.5\" ry=\"4.5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/></g>"
|
||||
},
|
||||
"wallet-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M12 8a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1z\"/><path d=\"M5.5 6A1.5 1.5 0 0 0 4 7.5v9A1.5 1.5 0 0 0 5.5 18h10a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 15.5 6zm2 7.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3\"/></g>"
|
||||
},
|
||||
"wallet-outline": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M15 17.5h3.005a1.5 1.5 0 0 0 1.5-1.5V8a1.5 1.5 0 0 0-1.5-1.5H15A1.5 1.5 0 0 1 16.5 8v8a1.5 1.5 0 0 1-1.5 1.5Z\"/><rect width=\"12\" height=\"11\" x=\"4.5\" y=\"6.5\" rx=\"1.5\"/><circle cx=\"8.75\" cy=\"11.75\" r=\"1.25\"/></g>"
|
||||
}
|
||||
},
|
||||
"suffixes": {
|
||||
"filled": "Filled",
|
||||
"outline": "Outline"
|
||||
},
|
||||
"width": 24,
|
||||
"height": 24
|
||||
}
|
||||
258
internal/db/sqlite-init/sqlite_init.go
Normal file
258
internal/db/sqlite-init/sqlite_init.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
_ "github.com/mattn/go-sqlite3" // Import the SQLite3 driver
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Icon struct {
|
||||
Body string `json:"body"`
|
||||
Width *int `json:"width,omitempty"`
|
||||
}
|
||||
|
||||
// Root Define the Root struct to represent the entire JSON structure
|
||||
type Root struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Icons map[string]Icon `json:"icons"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
testDbPath := os.Getenv("TEST_DB_PATH")
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
dbPaths := []string{testDbPath, dbPath}
|
||||
flaticonSvgDir := os.Getenv("SVG_DIR")
|
||||
//dbPath := "/Users/donov/Desktop/nkode.db"
|
||||
//dbPaths := []string{dbPath}
|
||||
//outputStr := MakeSvgFiles()
|
||||
for _, path := range dbPaths {
|
||||
MakeTables(path)
|
||||
FlaticonToSqlite(path, flaticonSvgDir)
|
||||
//SvgToSqlite(path, outputStr)
|
||||
}
|
||||
}
|
||||
|
||||
func FlaticonToSqlite(dbPath string, svgDir string) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Open the directory
|
||||
files, err := os.ReadDir(svgDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// Check if it is a regular file (not a directory) and has a .svg extension
|
||||
if file.IsDir() || filepath.Ext(file.Name()) != ".svg" {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(svgDir, file.Name())
|
||||
|
||||
// Read the file contents
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Println("Error reading file:", filePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the file name and first few bytes of the file content
|
||||
insertSql := `
|
||||
INSERT INTO svg_icon (svg)
|
||||
VALUES (?)
|
||||
`
|
||||
_, err = db.Exec(insertSql, string(content))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SvgToSqlite(dbPath string, outputStr string) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
insertSql := `
|
||||
INSERT INTO svg_icon (svg)
|
||||
VALUES (?)
|
||||
`
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
_, err := db.Exec(insertSql, line)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func MakeSvgFiles() string {
|
||||
jsonFiles, err := GetAllFiles("./core/sqlite-init/json")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting JSON files: %v", err)
|
||||
}
|
||||
|
||||
if len(jsonFiles) == 0 {
|
||||
log.Fatal("No JSON files found in ./json folder")
|
||||
}
|
||||
|
||||
var outputStr string
|
||||
strSet := make(map[string]struct{})
|
||||
for _, filename := range jsonFiles {
|
||||
fileData, err := LoadJson(filename)
|
||||
if err != nil {
|
||||
log.Print("Error loading JSON file: ", err)
|
||||
continue
|
||||
}
|
||||
height := fileData.Height
|
||||
for name, icon := range fileData.Icons {
|
||||
|
||||
width := height
|
||||
parts := strings.Split(name, "-")
|
||||
if len(parts) <= 0 {
|
||||
log.Print(name, " has no parts")
|
||||
continue
|
||||
}
|
||||
part0 := parts[0]
|
||||
_, exists := strSet[part0]
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
if icon.Width != nil {
|
||||
width = *icon.Width
|
||||
}
|
||||
strSet[part0] = struct{}{}
|
||||
if icon.Body == "" {
|
||||
continue
|
||||
}
|
||||
outputStr = fmt.Sprintf("%s<svg viewBox=\"0 0 %d %d\" xmlns=\"http://www.w3.org/2000/svg\">%s</svg>\n", outputStr, width, height, icon.Body)
|
||||
}
|
||||
}
|
||||
return outputStr
|
||||
}
|
||||
|
||||
func GetAllFiles(dir string) ([]string, error) {
|
||||
// Use ioutil.ReadDir to list all files in the directory
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read directory: %v", err)
|
||||
}
|
||||
|
||||
// Create a slice to hold the JSON filenames
|
||||
var jsonFiles []string
|
||||
|
||||
// Loop through the files and filter out JSON files
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
|
||||
jsonFiles = append(jsonFiles, filepath.Join(dir, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return jsonFiles, nil
|
||||
}
|
||||
|
||||
func LoadJson(filename string) (*Root, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %v", filename, err)
|
||||
}
|
||||
|
||||
var root Root
|
||||
err = json.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
return &root, nil
|
||||
}
|
||||
|
||||
func MakeTables(dbPath string) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
createTable := `
|
||||
PRAGMA journal_mode=WAL;
|
||||
--PRAGMA busy_timeout = 5000; -- Wait up to 5 seconds
|
||||
--PRAGMA synchronous = NORMAL; -- Reduce sync frequency for less locking
|
||||
--PRAGMA cache_size = -16000; -- Increase cache size (16MB)PRAGMA
|
||||
|
||||
CREATE TABLE IF NOT EXISTS customer (
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
,max_nkode_len INTEGER NOT NULL
|
||||
,min_nkode_len INTEGER NOT NULL
|
||||
,distinct_sets INTEGER NOT NULL
|
||||
,distinct_attributes INTEGER NOT NULL
|
||||
,lock_out INTEGER NOT NULL
|
||||
,expiration INTEGER NOT NULL
|
||||
,attribute_values BLOB NOT NULL
|
||||
,set_values BLOB NOT NULL
|
||||
,last_renew TEXT NOT NULL
|
||||
,created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
,email TEXT NOT NULL
|
||||
-- first_name TEXT NOT NULL
|
||||
-- last_name TEXT NOT NULL
|
||||
,renew INT NOT NULL
|
||||
,refresh_token TEXT
|
||||
,customer_id TEXT NOT NULL
|
||||
|
||||
-- Enciphered Passcode
|
||||
,code TEXT NOT NULL
|
||||
,mask TEXT NOT NULL
|
||||
|
||||
-- Keypad Dimensions
|
||||
,attributes_per_key INT NOT NULL
|
||||
,number_of_keys INT NOT NULL
|
||||
|
||||
-- User Keys
|
||||
,alpha_key BLOB NOT NULL
|
||||
,set_key BLOB NOT NULL
|
||||
,pass_key BLOB NOT NULL
|
||||
,mask_key BLOB NOT NULL
|
||||
,salt BLOB NOT NULL
|
||||
,max_nkode_len INT NOT NULL
|
||||
|
||||
-- User Interface
|
||||
,idx_interface BLOB NOT NULL
|
||||
,svg_id_interface BLOB NOT NULL
|
||||
|
||||
,last_login TEXT NULL
|
||||
,created_at TEXT
|
||||
|
||||
,FOREIGN KEY (customer_id) REFERENCES customer(id)
|
||||
,UNIQUE(customer_id, email)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS svg_icon (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,svg TEXT NOT NULL
|
||||
);
|
||||
|
||||
`
|
||||
_, err = db.Exec(createTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
564
internal/db/sqlite_db.go
Normal file
564
internal/db/sqlite_db.go
Normal file
@@ -0,0 +1,564 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3" // Import the SQLite3 driver
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/models"
|
||||
"go-nkode/internal/security"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SqliteDB struct {
|
||||
db *sql.DB
|
||||
stop bool
|
||||
writeQueue chan WriteTx
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type WriteTx struct {
|
||||
ErrChan chan error
|
||||
Query string
|
||||
Args []any
|
||||
}
|
||||
|
||||
const (
|
||||
writeBuffer = 1000
|
||||
)
|
||||
|
||||
func NewSqliteDB(path string) *SqliteDB {
|
||||
db, err := sql.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
log.Fatal("database didn't open ", err)
|
||||
}
|
||||
sqldb := SqliteDB{
|
||||
db: db,
|
||||
stop: false,
|
||||
writeQueue: make(chan WriteTx, writeBuffer),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for writeTx := range sqldb.writeQueue {
|
||||
writeTx.ErrChan <- sqldb.writeToDb(writeTx.Query, writeTx.Args)
|
||||
sqldb.wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
return &sqldb
|
||||
}
|
||||
|
||||
func (d *SqliteDB) CloseDb() {
|
||||
d.stop = true
|
||||
d.wg.Wait()
|
||||
if err := d.db.Close(); err != nil {
|
||||
// If db.Close() returns an error, panic
|
||||
panic(fmt.Sprintf("Failed to close the database: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SqliteDB) WriteNewCustomer(c models.Customer) error {
|
||||
query := `
|
||||
INSERT INTO customer (
|
||||
id
|
||||
,max_nkode_len
|
||||
,min_nkode_len
|
||||
,distinct_sets
|
||||
,distinct_attributes
|
||||
,lock_out
|
||||
,expiration
|
||||
,attribute_values
|
||||
,set_values
|
||||
,last_renew
|
||||
,created_at
|
||||
)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
`
|
||||
args := []any{
|
||||
uuid.UUID(c.Id), c.NKodePolicy.MaxNkodeLen, c.NKodePolicy.MinNkodeLen, c.NKodePolicy.DistinctSets,
|
||||
c.NKodePolicy.DistinctAttributes, c.NKodePolicy.LockOut, c.NKodePolicy.Expiration,
|
||||
c.Attributes.AttrBytes(), c.Attributes.SetBytes(), timeStamp(), timeStamp(),
|
||||
}
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) WriteNewUser(u models.User) error {
|
||||
query := `
|
||||
INSERT INTO user (
|
||||
id
|
||||
,email
|
||||
,renew
|
||||
,refresh_token
|
||||
,customer_id
|
||||
,code
|
||||
,mask
|
||||
,attributes_per_key
|
||||
,number_of_keys
|
||||
,alpha_key
|
||||
,set_key
|
||||
,pass_key
|
||||
,mask_key
|
||||
,salt
|
||||
,max_nkode_len
|
||||
,idx_interface
|
||||
,svg_id_interface
|
||||
,created_at
|
||||
)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`
|
||||
var renew int
|
||||
if u.Renew {
|
||||
renew = 1
|
||||
} else {
|
||||
renew = 0
|
||||
}
|
||||
|
||||
args := []any{
|
||||
uuid.UUID(u.Id), u.Email, renew, u.RefreshToken, uuid.UUID(u.CustomerId),
|
||||
u.EncipheredPasscode.Code, u.EncipheredPasscode.Mask, u.Kp.AttrsPerKey, u.Kp.NumbOfKeys,
|
||||
security.Uint64ArrToByteArr(u.CipherKeys.AlphaKey), security.Uint64ArrToByteArr(u.CipherKeys.SetKey),
|
||||
security.Uint64ArrToByteArr(u.CipherKeys.PassKey), security.Uint64ArrToByteArr(u.CipherKeys.MaskKey),
|
||||
u.CipherKeys.Salt, u.CipherKeys.MaxNKodeLen, security.IntArrToByteArr(u.Interface.IdxInterface),
|
||||
security.IntArrToByteArr(u.Interface.SvgId), timeStamp(),
|
||||
}
|
||||
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) UpdateUserNKode(u models.User) error {
|
||||
query := `
|
||||
UPDATE user
|
||||
SET renew = ?
|
||||
,refresh_token = ?
|
||||
,code = ?
|
||||
,mask = ?
|
||||
,attributes_per_key = ?
|
||||
,number_of_keys = ?
|
||||
,alpha_key = ?
|
||||
,set_key = ?
|
||||
,pass_key = ?
|
||||
,mask_key = ?
|
||||
,salt = ?
|
||||
,max_nkode_len = ?
|
||||
,idx_interface = ?
|
||||
,svg_id_interface = ?
|
||||
WHERE email = ? AND customer_id = ?
|
||||
`
|
||||
var renew int
|
||||
if u.Renew {
|
||||
renew = 1
|
||||
} else {
|
||||
renew = 0
|
||||
}
|
||||
args := []any{renew, u.RefreshToken, u.EncipheredPasscode.Code, u.EncipheredPasscode.Mask, u.Kp.AttrsPerKey, u.Kp.NumbOfKeys, security.Uint64ArrToByteArr(u.CipherKeys.AlphaKey), security.Uint64ArrToByteArr(u.CipherKeys.SetKey), security.Uint64ArrToByteArr(u.CipherKeys.PassKey), security.Uint64ArrToByteArr(u.CipherKeys.MaskKey), u.CipherKeys.Salt, u.CipherKeys.MaxNKodeLen, security.IntArrToByteArr(u.Interface.IdxInterface), security.IntArrToByteArr(u.Interface.SvgId), string(u.Email), uuid.UUID(u.CustomerId)}
|
||||
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) UpdateUserInterface(id models.UserId, ui models.UserInterface) error {
|
||||
query := `
|
||||
UPDATE user SET idx_interface = ?, last_login = ? WHERE id = ?
|
||||
`
|
||||
args := []any{security.IntArrToByteArr(ui.IdxInterface), timeStamp(), uuid.UUID(id).String()}
|
||||
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) UpdateUserRefreshToken(id models.UserId, refreshToken string) error {
|
||||
query := `
|
||||
UPDATE user SET refresh_token = ? WHERE id = ?
|
||||
`
|
||||
args := []any{refreshToken, uuid.UUID(id).String()}
|
||||
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) Renew(id models.CustomerId) error {
|
||||
// TODO: How long does a renew take?
|
||||
customer, err := d.GetCustomer(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setXor, attrXor, err := customer.RenewKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
renewArgs := []any{security.Uint64ArrToByteArr(customer.Attributes.AttrVals), security.Uint64ArrToByteArr(customer.Attributes.SetVals), uuid.UUID(customer.Id).String()}
|
||||
// TODO: replace with tx
|
||||
renewQuery := `
|
||||
UPDATE customer
|
||||
SET attribute_values = ?, set_values = ?
|
||||
WHERE id = ?;
|
||||
`
|
||||
|
||||
userQuery := `
|
||||
SELECT
|
||||
id
|
||||
,alpha_key
|
||||
,set_key
|
||||
,attributes_per_key
|
||||
,number_of_keys
|
||||
FROM user
|
||||
WHERE customer_id = ?
|
||||
`
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := tx.Query(userQuery, uuid.UUID(id).String())
|
||||
for rows.Next() {
|
||||
var userId string
|
||||
var alphaBytes []byte
|
||||
var setBytes []byte
|
||||
var attrsPerKey int
|
||||
var numbOfKeys int
|
||||
err = rows.Scan(&userId, &alphaBytes, &setBytes, &attrsPerKey, &numbOfKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := models.User{
|
||||
Id: models.UserId{},
|
||||
CustomerId: models.CustomerId{},
|
||||
Email: "",
|
||||
EncipheredPasscode: models.EncipheredNKode{},
|
||||
Kp: models.KeypadDimension{
|
||||
AttrsPerKey: attrsPerKey,
|
||||
NumbOfKeys: numbOfKeys,
|
||||
},
|
||||
CipherKeys: models.UserCipherKeys{
|
||||
AlphaKey: security.ByteArrToUint64Arr(alphaBytes),
|
||||
SetKey: security.ByteArrToUint64Arr(setBytes),
|
||||
},
|
||||
Interface: models.UserInterface{},
|
||||
Renew: false,
|
||||
}
|
||||
err = user.RenewKeys(setXor, attrXor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
renewQuery += `
|
||||
UPDATE user
|
||||
SET alpha_key = ?, set_key = ?, renew = ?
|
||||
WHERE id = ?;
|
||||
`
|
||||
renewArgs = append(renewArgs, security.Uint64ArrToByteArr(user.CipherKeys.AlphaKey), security.Uint64ArrToByteArr(user.CipherKeys.SetKey), 1, userId)
|
||||
}
|
||||
renewQuery += `
|
||||
`
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.addWriteTx(renewQuery, renewArgs)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) RefreshUserPasscode(user models.User, passcodeIdx []int, customerAttr models.CustomerAttributes) error {
|
||||
err := user.RefreshPasscode(passcodeIdx, customerAttr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := `
|
||||
UPDATE user
|
||||
SET
|
||||
renew = ?
|
||||
,code = ?
|
||||
,mask = ?
|
||||
,alpha_key = ?
|
||||
,set_key = ?
|
||||
,pass_key = ?
|
||||
,mask_key = ?
|
||||
,salt = ?
|
||||
WHERE id = ?;
|
||||
`
|
||||
args := []any{user.RefreshToken, 0, user.EncipheredPasscode.Code, user.EncipheredPasscode.Mask, security.Uint64ArrToByteArr(user.CipherKeys.AlphaKey), security.Uint64ArrToByteArr(user.CipherKeys.SetKey), security.Uint64ArrToByteArr(user.CipherKeys.PassKey), security.Uint64ArrToByteArr(user.CipherKeys.MaskKey), user.CipherKeys.Salt, uuid.UUID(user.Id).String()}
|
||||
return d.addWriteTx(query, args)
|
||||
}
|
||||
func (d *SqliteDB) GetCustomer(id models.CustomerId) (*models.Customer, error) {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Write new user won't roll back %+v", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
selectCustomer := `
|
||||
SELECT
|
||||
max_nkode_len
|
||||
,min_nkode_len
|
||||
,distinct_sets
|
||||
,distinct_attributes
|
||||
,lock_out
|
||||
,expiration
|
||||
,attribute_values
|
||||
,set_values
|
||||
FROM customer
|
||||
WHERE id = ?
|
||||
`
|
||||
rows, err := tx.Query(selectCustomer, uuid.UUID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
log.Printf("no new row for customer %s with err %s", id, rows.Err())
|
||||
return nil, config.ErrCustomerDne
|
||||
}
|
||||
|
||||
var maxNKodeLen int
|
||||
var minNKodeLen int
|
||||
var distinctSets int
|
||||
var distinctAttributes int
|
||||
var lockOut int
|
||||
var expiration int
|
||||
var attributeValues []byte
|
||||
var setValues []byte
|
||||
err = rows.Scan(&maxNKodeLen, &minNKodeLen, &distinctSets, &distinctAttributes, &lockOut, &expiration, &attributeValues, &setValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customer := models.Customer{
|
||||
Id: id,
|
||||
NKodePolicy: models.NKodePolicy{
|
||||
MaxNkodeLen: maxNKodeLen,
|
||||
MinNkodeLen: minNKodeLen,
|
||||
DistinctSets: distinctSets,
|
||||
DistinctAttributes: distinctAttributes,
|
||||
LockOut: lockOut,
|
||||
Expiration: expiration,
|
||||
},
|
||||
Attributes: models.NewCustomerAttributesFromBytes(attributeValues, setValues),
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &customer, nil
|
||||
}
|
||||
|
||||
func (d *SqliteDB) GetUser(email models.UserEmail, customerId models.CustomerId) (*models.User, error) {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userSelect := `
|
||||
SELECT
|
||||
id
|
||||
,renew
|
||||
,refresh_token
|
||||
,code
|
||||
,mask
|
||||
,attributes_per_key
|
||||
,number_of_keys
|
||||
,alpha_key
|
||||
,set_key
|
||||
,pass_key
|
||||
,mask_key
|
||||
,salt
|
||||
,max_nkode_len
|
||||
,idx_interface
|
||||
,svg_id_interface
|
||||
FROM user
|
||||
WHERE user.email = ? AND user.customer_id = ?
|
||||
`
|
||||
rows, err := tx.Query(userSelect, string(email), uuid.UUID(customerId).String())
|
||||
if !rows.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
var (
|
||||
id string
|
||||
renewVal int
|
||||
refreshToken string
|
||||
code string
|
||||
mask string
|
||||
attrsPerKey int
|
||||
numbOfKeys int
|
||||
alphaKey []byte
|
||||
setKey []byte
|
||||
passKey []byte
|
||||
maskKey []byte
|
||||
salt []byte
|
||||
maxNKodeLen int
|
||||
idxInterface []byte
|
||||
svgIdInterface []byte
|
||||
)
|
||||
err = rows.Scan(&id, &renewVal, &refreshToken, &code, &mask, &attrsPerKey, &numbOfKeys, &alphaKey, &setKey, &passKey, &maskKey, &salt, &maxNKodeLen, &idxInterface, &svgIdInterface)
|
||||
|
||||
userId, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var renew bool
|
||||
if renewVal == 0 {
|
||||
renew = false
|
||||
} else {
|
||||
renew = true
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Id: models.UserId(userId),
|
||||
CustomerId: customerId,
|
||||
Email: email,
|
||||
EncipheredPasscode: models.EncipheredNKode{
|
||||
Code: code,
|
||||
Mask: mask,
|
||||
},
|
||||
Kp: models.KeypadDimension{
|
||||
AttrsPerKey: attrsPerKey,
|
||||
NumbOfKeys: numbOfKeys,
|
||||
},
|
||||
CipherKeys: models.UserCipherKeys{
|
||||
AlphaKey: security.ByteArrToUint64Arr(alphaKey),
|
||||
SetKey: security.ByteArrToUint64Arr(setKey),
|
||||
PassKey: security.ByteArrToUint64Arr(passKey),
|
||||
MaskKey: security.ByteArrToUint64Arr(maskKey),
|
||||
Salt: salt,
|
||||
MaxNKodeLen: maxNKodeLen,
|
||||
Kp: nil,
|
||||
},
|
||||
Interface: models.UserInterface{
|
||||
IdxInterface: security.ByteArrToIntArr(idxInterface),
|
||||
SvgId: security.ByteArrToIntArr(svgIdInterface),
|
||||
Kp: nil,
|
||||
},
|
||||
Renew: renew,
|
||||
RefreshToken: refreshToken,
|
||||
}
|
||||
user.Interface.Kp = &user.Kp
|
||||
user.CipherKeys.Kp = &user.Kp
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *SqliteDB) RandomSvgInterface(kp models.KeypadDimension) ([]string, error) {
|
||||
ids, err := d.getRandomIds(kp.TotalAttrs())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getSvgsById(ids)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) RandomSvgIdxInterface(kp models.KeypadDimension) (models.SvgIdInterface, error) {
|
||||
return d.getRandomIds(kp.TotalAttrs())
|
||||
}
|
||||
|
||||
func (d *SqliteDB) GetSvgStringInterface(idxs models.SvgIdInterface) ([]string, error) {
|
||||
return d.getSvgsById(idxs)
|
||||
}
|
||||
|
||||
func (d *SqliteDB) getSvgsById(ids []int) ([]string, error) {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectId := `
|
||||
SELECT svg
|
||||
FROM svg_icon
|
||||
WHERE id = ?
|
||||
`
|
||||
svgs := make([]string, len(ids))
|
||||
for idx, id := range ids {
|
||||
rows, err := tx.Query(selectId, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !rows.Next() {
|
||||
log.Printf("id not found: %d", id)
|
||||
return nil, config.ErrSvgDne
|
||||
}
|
||||
if err = rows.Scan(&svgs[idx]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svgs, nil
|
||||
}
|
||||
|
||||
func (d *SqliteDB) writeToDb(query string, args []any) error {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Fatalf("fatal error: write won't roll back %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if _, err = tx.Exec(query, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SqliteDB) addWriteTx(query string, args []any) error {
|
||||
if d.stop {
|
||||
return config.ErrStoppingDatabase
|
||||
}
|
||||
errChan := make(chan error)
|
||||
writeTx := WriteTx{
|
||||
Query: query,
|
||||
Args: args,
|
||||
ErrChan: errChan,
|
||||
}
|
||||
d.wg.Add(1)
|
||||
d.writeQueue <- writeTx
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
func (d *SqliteDB) getRandomIds(count int) ([]int, error) {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil, config.ErrSqliteTx
|
||||
}
|
||||
rows, err := tx.Query("SELECT COUNT(*) as count FROM svg_icon;")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil, config.ErrSqliteTx
|
||||
}
|
||||
var tableLen int
|
||||
if !rows.Next() {
|
||||
return nil, config.ErrEmptySvgTable
|
||||
}
|
||||
|
||||
if err = rows.Scan(&tableLen); err != nil {
|
||||
log.Print(err)
|
||||
return nil, config.ErrSqliteTx
|
||||
}
|
||||
perm, err := security.RandomPermutation(tableLen)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for idx := range perm {
|
||||
perm[idx] += 1
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Print(err)
|
||||
return nil, config.ErrSqliteTx
|
||||
}
|
||||
|
||||
return perm[:count], nil
|
||||
}
|
||||
|
||||
func timeStamp() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
60
internal/db/sqlite_db_test.go
Normal file
60
internal/db/sqlite_db_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go-nkode/internal/api"
|
||||
"go-nkode/internal/models"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewSqliteDB(t *testing.T) {
|
||||
dbFile := os.Getenv("TEST_DB")
|
||||
// sql_driver.MakeTables(dbFile)
|
||||
db := NewSqliteDB(dbFile)
|
||||
defer db.CloseDb()
|
||||
|
||||
testSignupLoginRenew(t, db)
|
||||
testSqliteDBRandomSvgInterface(t, db)
|
||||
// if _, err := os.Stat(dbFile); err == nil {
|
||||
// err = os.Remove(dbFile)
|
||||
// assert.NoError(t, err)
|
||||
// } else {
|
||||
// assert.NoError(t, err)
|
||||
// }
|
||||
}
|
||||
|
||||
func testSignupLoginRenew(t *testing.T, db api.DbAccessor) {
|
||||
nkodePolicy := models.NewDefaultNKodePolicy()
|
||||
customerOrig, err := models.NewCustomer(nkodePolicy)
|
||||
assert.NoError(t, err)
|
||||
err = db.WriteNewCustomer(*customerOrig)
|
||||
assert.NoError(t, err)
|
||||
customer, err := db.GetCustomer(customerOrig.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, customerOrig, customer)
|
||||
username := "test_user@example.com"
|
||||
kp := models.KeypadDefault
|
||||
passcodeIdx := []int{0, 1, 2, 3}
|
||||
mockSvgInterface := make(models.SvgIdInterface, kp.TotalAttrs())
|
||||
ui, err := models.NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
userOrig, err := models.NewUser(*customer, username, passcodeIdx, *ui, kp)
|
||||
assert.NoError(t, err)
|
||||
err = db.WriteNewUser(*userOrig)
|
||||
assert.NoError(t, err)
|
||||
user, err := db.GetUser(models.UserEmail(username), customer.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, userOrig, user)
|
||||
|
||||
err = db.Renew(customer.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func testSqliteDBRandomSvgInterface(t *testing.T, db api.DbAccessor) {
|
||||
kp := models.KeypadMax
|
||||
svgs, err := db.RandomSvgInterface(kp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, svgs, kp.TotalAttrs())
|
||||
}
|
||||
168
internal/email/queue.go
Normal file
168
internal/email/queue.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ses"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ses/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
config2 "go-nkode/config"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EmailClient interface {
|
||||
SendEmail(Email) error
|
||||
}
|
||||
|
||||
// Email represents a dummy email structure
|
||||
type Email struct {
|
||||
Sender string
|
||||
Recipient string
|
||||
Subject string
|
||||
Content string
|
||||
}
|
||||
|
||||
type TestEmailClient struct{}
|
||||
|
||||
// SendEmail simulates sending an email via AWS SES
|
||||
func (c *TestEmailClient) SendEmail(email Email) error {
|
||||
// Simulate sending email (replace with actual AWS SES API call)
|
||||
fmt.Printf("Sending email to %s\n", email.Recipient)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SESClient struct {
|
||||
ResetCache *cache.Cache
|
||||
}
|
||||
|
||||
const (
|
||||
emailRetryExpiration = 5 * time.Minute
|
||||
sesCleanupInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
func NewSESClient() SESClient {
|
||||
return SESClient{
|
||||
ResetCache: cache.New(emailRetryExpiration, sesCleanupInterval),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SESClient) SendEmail(email Email) error {
|
||||
if _, exists := s.ResetCache.Get(email.Recipient); exists {
|
||||
log.Printf("email already sent to %s with subject %s", email.Recipient, email.Subject)
|
||||
return config2.ErrEmailAlreadySent
|
||||
}
|
||||
|
||||
// Load AWS configuration
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1"))
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("unable to load SDK config, %v", err)
|
||||
log.Print(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an SES client
|
||||
sesClient := ses.NewFromConfig(cfg)
|
||||
|
||||
// Construct the email message
|
||||
input := &ses.SendEmailInput{
|
||||
Destination: &types.Destination{
|
||||
ToAddresses: []string{email.Recipient},
|
||||
},
|
||||
Message: &types.Message{
|
||||
Body: &types.Body{
|
||||
Html: &types.Content{
|
||||
Data: aws.String(email.Content),
|
||||
},
|
||||
},
|
||||
Subject: &types.Content{
|
||||
Data: aws.String(email.Subject),
|
||||
},
|
||||
},
|
||||
Source: aws.String(email.Sender),
|
||||
}
|
||||
|
||||
if err = s.ResetCache.Add(email.Recipient, nil, emailRetryExpiration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the email
|
||||
resp, err := sesClient.SendEmail(context.TODO(), input)
|
||||
if err != nil {
|
||||
s.ResetCache.Delete(email.Recipient)
|
||||
errMsg := fmt.Sprintf("failed to send email, %v", err)
|
||||
log.Print(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Output the message ID of the sent email
|
||||
fmt.Printf("UserEmail sent successfully, Message ID: %s\n", *resp.MessageId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmailQueue represents the email queue with rate limiting
|
||||
type EmailQueue struct {
|
||||
stop bool
|
||||
emailQueue chan Email // Email queue
|
||||
rateLimit <-chan time.Time // Rate limiter
|
||||
client EmailClient // SES client to send emails
|
||||
wg sync.WaitGroup // To wait for all emails to be processed
|
||||
FailedSendCount int
|
||||
}
|
||||
|
||||
// NewEmailQueue creates a new rate-limited email queue
|
||||
func NewEmailQueue(bufferSize int, emailsPerSecond int, client EmailClient) *EmailQueue {
|
||||
// Create a ticker that ticks every second to limit the rate of sending emails
|
||||
rateLimit := time.Tick(time.Second / time.Duration(emailsPerSecond))
|
||||
|
||||
return &EmailQueue{
|
||||
stop: false,
|
||||
emailQueue: make(chan Email, bufferSize),
|
||||
rateLimit: rateLimit,
|
||||
client: client,
|
||||
FailedSendCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// AddEmail queues a new email to be sent
|
||||
func (q *EmailQueue) AddEmail(email Email) {
|
||||
if q.stop {
|
||||
log.Printf("email %s with subject %s not add. Stopping queue", email.Recipient, email.Subject)
|
||||
return
|
||||
}
|
||||
q.wg.Add(1)
|
||||
q.emailQueue <- email
|
||||
}
|
||||
|
||||
// Start begins processing the email queue with rate limiting
|
||||
func (q *EmailQueue) Start() {
|
||||
q.stop = false
|
||||
// Worker goroutine that processes emails from the queue
|
||||
go func() {
|
||||
for email := range q.emailQueue {
|
||||
<-q.rateLimit // Wait for the rate limiter to allow the next email
|
||||
q.sendEmail(email)
|
||||
q.wg.Done() // Mark the email as processed
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// sendEmail sends an email using the SES client
|
||||
func (q *EmailQueue) sendEmail(email Email) {
|
||||
if err := q.client.SendEmail(email); err != nil {
|
||||
q.FailedSendCount += 1
|
||||
log.Printf("Failed to send email to %s: %v\n", email.Recipient, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the queue after all emails have been processed
|
||||
func (q *EmailQueue) Stop() {
|
||||
q.stop = true
|
||||
// Wait for all emails to be processed
|
||||
q.wg.Wait()
|
||||
// Close the email queue
|
||||
close(q.emailQueue)
|
||||
}
|
||||
29
internal/email/queue_test.go
Normal file
29
internal/email/queue_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmailQueue(t *testing.T) {
|
||||
queue := NewEmailQueue(100, 14, &TestEmailClient{})
|
||||
|
||||
// Start the queue processing
|
||||
queue.Start()
|
||||
|
||||
// Enqueue some emails
|
||||
for i := 1; i <= 28; i++ {
|
||||
email := Email{
|
||||
Sender: "test@example.com",
|
||||
Recipient: fmt.Sprintf("user%d@example.com", i),
|
||||
Subject: "test subject",
|
||||
Content: "This is a test email",
|
||||
}
|
||||
queue.AddEmail(email)
|
||||
}
|
||||
// CloseDb the queue after all emails are processed
|
||||
queue.Stop()
|
||||
|
||||
assert.Equal(t, queue.FailedSendCount, 0)
|
||||
}
|
||||
84
internal/models/customer.go
Normal file
84
internal/models/customer.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/security"
|
||||
"go-nkode/internal/utils"
|
||||
)
|
||||
|
||||
type Customer struct {
|
||||
Id CustomerId
|
||||
NKodePolicy NKodePolicy
|
||||
Attributes CustomerAttributes
|
||||
}
|
||||
|
||||
func NewCustomer(nkodePolicy NKodePolicy) (*Customer, error) {
|
||||
customerAttrs, err := NewCustomerAttributes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customer := Customer{
|
||||
Id: CustomerId(uuid.New()),
|
||||
NKodePolicy: nkodePolicy,
|
||||
Attributes: *customerAttrs,
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
}
|
||||
|
||||
func (c *Customer) IsValidNKode(kp KeypadDimension, passcodeAttrIdx []int) error {
|
||||
nkodeLen := len(passcodeAttrIdx)
|
||||
if nkodeLen < c.NKodePolicy.MinNkodeLen || nkodeLen > c.NKodePolicy.MaxNkodeLen {
|
||||
return config.ErrInvalidNKodeLength
|
||||
}
|
||||
|
||||
if validIdx := kp.ValidateAttributeIndices(passcodeAttrIdx); !validIdx {
|
||||
return config.ErrInvalidNKodeIdx
|
||||
}
|
||||
passcodeSetVals := make(utils.Set[uint64])
|
||||
passcodeAttrVals := make(utils.Set[uint64])
|
||||
attrVals, err := c.Attributes.AttrValsForKp(kp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx := 0; idx < nkodeLen; idx++ {
|
||||
attrVal := attrVals[passcodeAttrIdx[idx]]
|
||||
setVal, err := c.Attributes.GetAttrSetVal(attrVal, kp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passcodeSetVals.Add(setVal)
|
||||
passcodeAttrVals.Add(attrVal)
|
||||
}
|
||||
|
||||
if passcodeSetVals.Size() < c.NKodePolicy.DistinctSets {
|
||||
return config.ErrTooFewDistinctSet
|
||||
}
|
||||
|
||||
if passcodeAttrVals.Size() < c.NKodePolicy.DistinctAttributes {
|
||||
return config.ErrTooFewDistinctAttributes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Customer) RenewKeys() ([]uint64, []uint64, error) {
|
||||
oldAttrs := make([]uint64, len(c.Attributes.AttrVals))
|
||||
oldSets := make([]uint64, len(c.Attributes.SetVals))
|
||||
|
||||
copy(oldAttrs, c.Attributes.AttrVals)
|
||||
copy(oldSets, c.Attributes.SetVals)
|
||||
|
||||
if err := c.Attributes.Renew(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
attrsXor, err := security.XorLists(oldAttrs, c.Attributes.AttrVals)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
setXor, err := security.XorLists(oldSets, c.Attributes.SetVals)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return setXor, attrsXor, nil
|
||||
}
|
||||
94
internal/models/customer_attributes.go
Normal file
94
internal/models/customer_attributes.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"go-nkode/internal/security"
|
||||
"log"
|
||||
)
|
||||
|
||||
type CustomerAttributes struct {
|
||||
AttrVals []uint64
|
||||
SetVals []uint64
|
||||
}
|
||||
|
||||
func NewCustomerAttributes() (*CustomerAttributes, error) {
|
||||
attrVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.TotalAttrs())
|
||||
if err != nil {
|
||||
log.Print("unable to generate attribute vals: ", err)
|
||||
return nil, err
|
||||
}
|
||||
setVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.AttrsPerKey)
|
||||
if err != nil {
|
||||
log.Print("unable to generate set vals: ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
customerAttrs := CustomerAttributes{
|
||||
AttrVals: attrVals,
|
||||
SetVals: setVals,
|
||||
}
|
||||
return &customerAttrs, nil
|
||||
}
|
||||
|
||||
func NewCustomerAttributesFromBytes(attrBytes []byte, setBytes []byte) CustomerAttributes {
|
||||
return CustomerAttributes{
|
||||
AttrVals: security.ByteArrToUint64Arr(attrBytes),
|
||||
SetVals: security.ByteArrToUint64Arr(setBytes),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) Renew() error {
|
||||
attrVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.TotalAttrs())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.AttrsPerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.AttrVals = attrVals
|
||||
c.SetVals = setVals
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) IndexOfAttr(attrVal uint64) (int, error) {
|
||||
// TODO: should this be mapped instead?
|
||||
return security.IndexOf[uint64](c.AttrVals, attrVal)
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) IndexOfSet(setVal uint64) (int, error) {
|
||||
// TODO: should this be mapped instead?
|
||||
return security.IndexOf[uint64](c.SetVals, setVal)
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) GetAttrSetVal(attrVal uint64, userKeypad KeypadDimension) (uint64, error) {
|
||||
indexOfAttr, err := c.IndexOfAttr(attrVal)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setIdx := indexOfAttr % userKeypad.AttrsPerKey
|
||||
return c.SetVals[setIdx], nil
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) AttrValsForKp(userKp KeypadDimension) ([]uint64, error) {
|
||||
err := userKp.IsValidKeypadDimension()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.AttrVals[:userKp.TotalAttrs()], nil
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) SetValsForKp(userKp KeypadDimension) ([]uint64, error) {
|
||||
err := userKp.IsValidKeypadDimension()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.SetVals[:userKp.AttrsPerKey], nil
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) AttrBytes() []byte {
|
||||
return security.Uint64ArrToByteArr(c.AttrVals)
|
||||
}
|
||||
|
||||
func (c *CustomerAttributes) SetBytes() []byte {
|
||||
return security.Uint64ArrToByteArr(c.SetVals)
|
||||
}
|
||||
57
internal/models/customer_test.go
Normal file
57
internal/models/customer_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCustomer(t *testing.T) {
|
||||
testNewCustomerAttributes(t)
|
||||
testCustomerValidKeyEntry(t)
|
||||
testCustomerIsValidNKode(t)
|
||||
}
|
||||
|
||||
func testNewCustomerAttributes(t *testing.T) {
|
||||
_, nil := NewCustomerAttributes()
|
||||
assert.NoError(t, nil)
|
||||
}
|
||||
|
||||
func testCustomerValidKeyEntry(t *testing.T) {
|
||||
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 9}
|
||||
nkodePolicy := NewDefaultNKodePolicy()
|
||||
customer, err := NewCustomer(nkodePolicy)
|
||||
assert.NoError(t, err)
|
||||
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
|
||||
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
userEmail := "testing@example.com"
|
||||
passcodeIdx := []int{0, 1, 2, 3}
|
||||
user, err := NewUser(*customer, userEmail, passcodeIdx, *userInterface, kp)
|
||||
assert.NoError(t, err)
|
||||
userLoginInterface, err := user.GetLoginInterface()
|
||||
assert.NoError(t, err)
|
||||
selectedKeys, err := SelectKeyByAttrIdx(userLoginInterface, passcodeIdx, kp)
|
||||
assert.NoError(t, err)
|
||||
validatedPasscode, err := ValidKeyEntry(*user, *customer, selectedKeys)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(validatedPasscode), len(passcodeIdx))
|
||||
for idx := range validatedPasscode {
|
||||
assert.Equal(t, validatedPasscode[idx], passcodeIdx[idx])
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomerIsValidNKode(t *testing.T) {
|
||||
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 7}
|
||||
nkodePolicy := NewDefaultNKodePolicy()
|
||||
customer, err := NewCustomer(nkodePolicy)
|
||||
assert.NoError(t, err)
|
||||
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
|
||||
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
userEmail := "testing123@example.com"
|
||||
passcodeIdx := []int{0, 1, 2, 3}
|
||||
user, err := NewUser(*customer, userEmail, passcodeIdx, *userInterface, kp)
|
||||
assert.NoError(t, err)
|
||||
err = customer.IsValidNKode(user.Kp, passcodeIdx)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
55
internal/models/keypad_dimension.go
Normal file
55
internal/models/keypad_dimension.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"go-nkode/config"
|
||||
py "go-nkode/internal/utils"
|
||||
)
|
||||
|
||||
type KeypadDimension struct {
|
||||
AttrsPerKey int `json:"attrs_per_key"`
|
||||
NumbOfKeys int `json:"numb_of_keys"`
|
||||
}
|
||||
|
||||
func (kp *KeypadDimension) TotalAttrs() int {
|
||||
return kp.AttrsPerKey * kp.NumbOfKeys
|
||||
}
|
||||
|
||||
func (kp *KeypadDimension) IsDispersable() bool {
|
||||
return kp.AttrsPerKey <= kp.NumbOfKeys
|
||||
}
|
||||
|
||||
func (kp *KeypadDimension) IsValidKeypadDimension() error {
|
||||
if KeypadMin.AttrsPerKey > kp.AttrsPerKey || KeypadMax.AttrsPerKey < kp.AttrsPerKey || KeypadMin.NumbOfKeys > kp.NumbOfKeys || KeypadMax.NumbOfKeys < kp.NumbOfKeys {
|
||||
return config.ErrInvalidKeypadDimensions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kp *KeypadDimension) ValidKeySelections(selectedKeys []int) bool {
|
||||
return py.All[int](selectedKeys, func(idx int) bool {
|
||||
return 0 <= idx && idx < kp.NumbOfKeys
|
||||
})
|
||||
}
|
||||
|
||||
func (kp *KeypadDimension) ValidateAttributeIndices(attrIndicies []int) bool {
|
||||
return py.All[int](attrIndicies, func(i int) bool {
|
||||
return i >= 0 && i < kp.TotalAttrs()
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
KeypadMax = KeypadDimension{
|
||||
AttrsPerKey: 16,
|
||||
NumbOfKeys: 15,
|
||||
}
|
||||
|
||||
KeypadMin = KeypadDimension{
|
||||
AttrsPerKey: 5,
|
||||
NumbOfKeys: 4,
|
||||
}
|
||||
|
||||
KeypadDefault = KeypadDimension{
|
||||
AttrsPerKey: 14,
|
||||
NumbOfKeys: 7,
|
||||
}
|
||||
)
|
||||
159
internal/models/models.go
Normal file
159
internal/models/models.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"net/mail"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SetNKodeResp struct {
|
||||
UserInterface []int `json:"user_interface"`
|
||||
}
|
||||
|
||||
type RandomSvgInterfaceResp struct {
|
||||
Svgs []string `json:"svgs"`
|
||||
Colors []RGBColor `json:"colors"`
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type NewCustomerPost struct {
|
||||
NKodePolicy NKodePolicy `json:"nkode_policy"`
|
||||
}
|
||||
|
||||
type GenerateSignupRestInterfacePost struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
AttrsPerKey int `json:"attrs_per_key"`
|
||||
NumbOfKeys int `json:"numb_of_keys"`
|
||||
UserEmail string `json:"email"`
|
||||
Reset bool `json:"reset"`
|
||||
}
|
||||
|
||||
type SetNKodePost struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
KeySelection []int `json:"key_selection"`
|
||||
SessionId string `json:"session_id"`
|
||||
}
|
||||
|
||||
type ConfirmNKodePost struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
KeySelection KeySelection `json:"key_selection"`
|
||||
SessionId string `json:"session_id"`
|
||||
}
|
||||
|
||||
type GetLoginInterfacePost struct {
|
||||
UserEmail string `json:"email"`
|
||||
CustomerId string `json:"customer_id"`
|
||||
}
|
||||
|
||||
type LoginPost struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
UserEmail string `json:"email"`
|
||||
KeySelection KeySelection `json:"key_selection"`
|
||||
}
|
||||
|
||||
type RenewAttributesPost struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
}
|
||||
|
||||
type RefreshTokenPost struct {
|
||||
UserEmail string `json:"email"`
|
||||
CustomerId string `json:"customer_id"`
|
||||
}
|
||||
|
||||
type ResetNKodePost struct {
|
||||
UserEmail string `json:"email"`
|
||||
CustomerId string `json:"customer_id"`
|
||||
}
|
||||
|
||||
type CreateNewCustomerResp struct {
|
||||
CustomerId string `json:"customer_id"`
|
||||
}
|
||||
|
||||
type GenerateSignupResetInterfaceResp struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserIdxInterface IdxInterface `json:"user_interface"`
|
||||
SvgInterface []string `json:"svg_interface"`
|
||||
Colors []RGBColor `json:"colors"`
|
||||
}
|
||||
|
||||
type GetLoginInterfaceResp struct {
|
||||
UserIdxInterface IdxInterface `json:"user_interface"`
|
||||
SvgInterface []string `json:"svg_interface"`
|
||||
AttrsPerKey int `json:"attrs_per_key"`
|
||||
NumbOfKeys int `json:"numb_of_keys"`
|
||||
Colors []RGBColor `json:"colors"`
|
||||
}
|
||||
|
||||
type KeySelection []int
|
||||
|
||||
type CustomerId uuid.UUID
|
||||
|
||||
func CustomerIdToString(customerId CustomerId) string {
|
||||
customerUuid := uuid.UUID(customerId)
|
||||
return customerUuid.String()
|
||||
}
|
||||
|
||||
type SessionId uuid.UUID
|
||||
type UserId uuid.UUID
|
||||
|
||||
func (s *SessionId) String() string {
|
||||
id := uuid.UUID(*s)
|
||||
return id.String()
|
||||
|
||||
}
|
||||
|
||||
type UserEmail string
|
||||
|
||||
func ParseEmail(email string) (UserEmail, error) {
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return UserEmail(strings.ToLower(email)), err
|
||||
|
||||
}
|
||||
|
||||
type IdxInterface []int
|
||||
type SvgIdInterface []int
|
||||
|
||||
func SessionIdFromString(sessionId string) (SessionId, error) {
|
||||
id, err := uuid.Parse(sessionId)
|
||||
if err != nil {
|
||||
return SessionId{}, err
|
||||
}
|
||||
|
||||
return SessionId(id), nil
|
||||
}
|
||||
|
||||
type EncipheredNKode struct {
|
||||
Code string
|
||||
Mask string
|
||||
}
|
||||
|
||||
type RGBColor struct {
|
||||
Red int `json:"red"`
|
||||
Green int `json:"green"`
|
||||
Blue int `json:"blue"`
|
||||
}
|
||||
|
||||
var SetColors = []RGBColor{
|
||||
{0, 0, 0}, // Black
|
||||
{255, 0, 0}, // Red
|
||||
{0, 128, 0}, // Dark Green
|
||||
{0, 0, 255}, // Blue
|
||||
{244, 200, 60}, // Yellow
|
||||
{255, 0, 255}, // Magenta
|
||||
{0, 200, 200}, // Cyan
|
||||
{127, 0, 127}, // Purple
|
||||
{232, 92, 13}, // Orange
|
||||
{0, 127, 127}, // Teal
|
||||
{127, 127, 0}, // Olive
|
||||
{127, 0, 0}, // Dark Red
|
||||
{128, 128, 128}, // Gray
|
||||
{228, 102, 102}, // Dark Purple
|
||||
{185, 17, 240}, // Salmon
|
||||
{16, 200, 100}, // Green
|
||||
}
|
||||
34
internal/models/policy.go
Normal file
34
internal/models/policy.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import "go-nkode/config"
|
||||
|
||||
type NKodePolicy struct {
|
||||
MaxNkodeLen int `json:"max_nkode_len"`
|
||||
MinNkodeLen int `json:"min_nkode_len"`
|
||||
DistinctSets int `json:"distinct_sets"`
|
||||
DistinctAttributes int `json:"distinct_attributes"`
|
||||
LockOut int `json:"lock_out"`
|
||||
Expiration int `json:"expiration"` // seconds, -1 no expiration
|
||||
}
|
||||
|
||||
func NewDefaultNKodePolicy() NKodePolicy {
|
||||
return NKodePolicy{
|
||||
MinNkodeLen: 4,
|
||||
MaxNkodeLen: 4,
|
||||
DistinctSets: 0,
|
||||
DistinctAttributes: 4,
|
||||
LockOut: 5,
|
||||
Expiration: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NKodePolicy) ValidLength(nkodeLen int) error {
|
||||
|
||||
if nkodeLen < p.MinNkodeLen || nkodeLen > p.MaxNkodeLen {
|
||||
return config.ErrInvalidNKodeLength
|
||||
}
|
||||
// TODO: validate Max > Min
|
||||
// Validate lockout
|
||||
// Add Lockout To User
|
||||
return nil
|
||||
}
|
||||
23
internal/models/test_helper.go
Normal file
23
internal/models/test_helper.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-nkode/internal/security"
|
||||
)
|
||||
|
||||
func SelectKeyByAttrIdx(interfaceUser []int, passcodeIdxs []int, keypadSize KeypadDimension) ([]int, error) {
|
||||
selectedKeys := make([]int, len(passcodeIdxs))
|
||||
for idx := range passcodeIdxs {
|
||||
attrIdx, err := security.IndexOf[int](interfaceUser, passcodeIdxs[idx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyNumb := attrIdx / keypadSize.AttrsPerKey
|
||||
if keyNumb >= keypadSize.NumbOfKeys {
|
||||
return nil, errors.New(fmt.Sprintf("index key number: %d out of range 0-%d", keyNumb, keypadSize.NumbOfKeys-1))
|
||||
}
|
||||
selectedKeys[idx] = keyNumb
|
||||
}
|
||||
return selectedKeys, nil
|
||||
}
|
||||
145
internal/models/user.go
Normal file
145
internal/models/user.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/security"
|
||||
"log"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id UserId
|
||||
CustomerId CustomerId
|
||||
Email UserEmail
|
||||
EncipheredPasscode EncipheredNKode
|
||||
Kp KeypadDimension
|
||||
CipherKeys UserCipherKeys
|
||||
Interface UserInterface
|
||||
Renew bool
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
func (u *User) DecipherMask(setVals []uint64, passcodeLen int) ([]uint64, error) {
|
||||
return u.CipherKeys.DecipherMask(u.EncipheredPasscode.Mask, setVals, passcodeLen)
|
||||
}
|
||||
|
||||
func (u *User) RenewKeys(setXor []uint64, attrXor []uint64) error {
|
||||
u.Renew = true
|
||||
var err error
|
||||
u.CipherKeys.SetKey, err = security.XorLists(setXor[:u.Kp.AttrsPerKey], u.CipherKeys.SetKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.CipherKeys.AlphaKey, err = security.XorLists(attrXor[:u.Kp.TotalAttrs()], u.CipherKeys.AlphaKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *User) RefreshPasscode(passcodeAttrIdx []int, customerAttributes CustomerAttributes) error {
|
||||
setVals, err := customerAttributes.SetValsForKp(u.Kp)
|
||||
newKeys, err := NewUserCipherKeys(&u.Kp, setVals, u.CipherKeys.MaxNKodeLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encipheredPasscode, err := newKeys.EncipherNKode(passcodeAttrIdx, customerAttributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.CipherKeys = *newKeys
|
||||
u.EncipheredPasscode = *encipheredPasscode
|
||||
u.Renew = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) GetLoginInterface() ([]int, error) {
|
||||
err := u.Interface.PartialInterfaceShuffle()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.Interface.IdxInterface, nil
|
||||
}
|
||||
|
||||
func ValidKeyEntry(user User, customer Customer, selectedKeys []int) ([]int, error) {
|
||||
if validKeys := user.Kp.ValidKeySelections(selectedKeys); !validKeys {
|
||||
|
||||
return nil, config.ErrKeyIndexOutOfRange
|
||||
}
|
||||
|
||||
passcodeLen := len(selectedKeys)
|
||||
if err := customer.NKodePolicy.ValidLength(passcodeLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setVals, err := customer.Attributes.SetValsForKp(user.Kp)
|
||||
if err != nil {
|
||||
log.Printf("fatal error in validate key entry;invalid user keypad dimensions for user %s with error %v", user.Email, err)
|
||||
return nil, config.ErrInternalValidKeyEntry
|
||||
}
|
||||
|
||||
passcodeSetVals, err := user.DecipherMask(setVals, passcodeLen)
|
||||
if err != nil {
|
||||
log.Printf("fatal error in validate key entry;something when wrong deciphering mask;user email %s; error %v", user.Email, err)
|
||||
return nil, config.ErrInternalValidKeyEntry
|
||||
}
|
||||
presumedAttrIdxVals := make([]int, passcodeLen)
|
||||
|
||||
for idx := range presumedAttrIdxVals {
|
||||
keyNumb := selectedKeys[idx]
|
||||
setIdx, err := customer.Attributes.IndexOfSet(passcodeSetVals[idx])
|
||||
if err != nil {
|
||||
log.Printf("fatal error in validate key entry;something when wrong getting the IndexOfSet;user email %s; error %v", user.Email, err)
|
||||
return nil, config.ErrInternalValidKeyEntry
|
||||
}
|
||||
selectedAttrIdx, err := user.Interface.GetAttrIdxByKeyNumbSetIdx(setIdx, keyNumb)
|
||||
if err != nil {
|
||||
log.Printf("fatal error in validate key entry;something when wrong getting the GetAttrIdxByKeyNumbSetIdx;user email %s; error %v", user.Email, err)
|
||||
return nil, config.ErrInternalValidKeyEntry
|
||||
}
|
||||
presumedAttrIdxVals[idx] = selectedAttrIdx
|
||||
}
|
||||
err = customer.IsValidNKode(user.Kp, presumedAttrIdxVals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attrVals, err := customer.Attributes.AttrValsForKp(user.Kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = user.CipherKeys.ValidPassword(user.EncipheredPasscode.Code, presumedAttrIdxVals, attrVals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return presumedAttrIdxVals, nil
|
||||
}
|
||||
|
||||
func NewUser(customer Customer, userEmail string, passcodeIdx []int, ui UserInterface, kp KeypadDimension) (*User, error) {
|
||||
_, err := ParseEmail(userEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setVals, err := customer.Attributes.SetValsForKp(kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newKeys, err := NewUserCipherKeys(&kp, setVals, customer.NKodePolicy.MaxNkodeLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encipheredNKode, err := newKeys.EncipherNKode(passcodeIdx, customer.Attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser := User{
|
||||
Id: UserId(uuid.New()),
|
||||
Email: UserEmail(userEmail),
|
||||
EncipheredPasscode: *encipheredNKode,
|
||||
CipherKeys: *newKeys,
|
||||
Interface: ui,
|
||||
Kp: kp,
|
||||
CustomerId: customer.Id,
|
||||
}
|
||||
return &newUser, nil
|
||||
}
|
||||
193
internal/models/user_cipher_keys.go
Normal file
193
internal/models/user_cipher_keys.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/security"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserCipherKeys struct {
|
||||
AlphaKey []uint64
|
||||
SetKey []uint64
|
||||
PassKey []uint64
|
||||
MaskKey []uint64
|
||||
Salt []byte
|
||||
MaxNKodeLen int
|
||||
Kp *KeypadDimension
|
||||
}
|
||||
|
||||
func NewUserCipherKeys(kp *KeypadDimension, setVals []uint64, maxNKodeLen int) (*UserCipherKeys, error) {
|
||||
err := kp.IsValidKeypadDimension()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKey, err := security.GenerateRandomNonRepeatingUint64(kp.AttrsPerKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKey, err = security.XorLists(setKey, setVals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alphaKey, _ := security.GenerateRandomNonRepeatingUint64(kp.TotalAttrs())
|
||||
passKey, _ := security.GenerateRandomNonRepeatingUint64(maxNKodeLen)
|
||||
maskKey, _ := security.GenerateRandomNonRepeatingUint64(maxNKodeLen)
|
||||
salt, _ := security.RandomBytes(10)
|
||||
userCipherKeys := UserCipherKeys{
|
||||
AlphaKey: alphaKey,
|
||||
PassKey: passKey,
|
||||
MaskKey: maskKey,
|
||||
SetKey: setKey,
|
||||
Salt: salt,
|
||||
MaxNKodeLen: maxNKodeLen,
|
||||
Kp: kp,
|
||||
}
|
||||
return &userCipherKeys, nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) PadUserMask(userMask []uint64, setVals []uint64) ([]uint64, error) {
|
||||
if len(userMask) > u.MaxNKodeLen {
|
||||
return nil, config.ErrUserMaskTooLong
|
||||
}
|
||||
paddedUserMask := make([]uint64, len(userMask))
|
||||
copy(paddedUserMask, userMask)
|
||||
for i := len(userMask); i < u.MaxNKodeLen; i++ {
|
||||
paddedUserMask = append(paddedUserMask, setVals[i%len(setVals)])
|
||||
}
|
||||
return paddedUserMask, nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) ValidPassword(hashedPassword string, passcodeAttrIdx []int, attrVals []uint64) error {
|
||||
hashBytes := []byte(hashedPassword)
|
||||
passcodeCipher := u.encipherCode(passcodeAttrIdx, attrVals)
|
||||
passwordDigest := u.saltAndDigest(passcodeCipher)
|
||||
err := bcrypt.CompareHashAndPassword(hashBytes, passwordDigest)
|
||||
if err != nil {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return config.ErrInvalidNKode
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) EncipherSaltHashCode(passcodeAttrIdx []int, attrVals []uint64) (string, error) {
|
||||
passcodeCipher := u.encipherCode(passcodeAttrIdx, attrVals)
|
||||
|
||||
passcodeDigest := u.saltAndDigest(passcodeCipher)
|
||||
passcodeBytes, err := u.hashPasscode(passcodeDigest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(passcodeBytes), nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) encipherCode(passcodeAttrIdx []int, attrVals []uint64) []uint64 {
|
||||
passcodeLen := len(passcodeAttrIdx)
|
||||
|
||||
passcodeCipher := make([]uint64, u.MaxNKodeLen)
|
||||
for idx := 0; idx < passcodeLen; idx++ {
|
||||
attrIdx := passcodeAttrIdx[idx]
|
||||
alpha := u.AlphaKey[attrIdx]
|
||||
attrVal := attrVals[attrIdx]
|
||||
pass := u.PassKey[idx]
|
||||
passcodeCipher[idx] = alpha ^ pass ^ attrVal
|
||||
}
|
||||
return passcodeCipher
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) saltAndDigest(passcode []uint64) []byte {
|
||||
passcodeBytes := security.Uint64ArrToByteArr(passcode)
|
||||
passcodeBytes = append(passcodeBytes, u.Salt...)
|
||||
h := sha256.New()
|
||||
h.Write(passcodeBytes)
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) hashPasscode(passcodeDigest []byte) ([]byte, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(passcodeDigest, bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hashedPassword, nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) EncipherMask(passcodeSet []uint64, customerAttrs CustomerAttributes, userKp KeypadDimension) (string, error) {
|
||||
setVals, err := customerAttrs.SetValsForKp(userKp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
paddedPasscodeSets, err := u.PadUserMask(passcodeSet, setVals)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipheredMask := make([]uint64, len(paddedPasscodeSets))
|
||||
for idx := range paddedPasscodeSets {
|
||||
setIdx, err := customerAttrs.IndexOfSet(paddedPasscodeSets[idx])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
setKeyVal := u.SetKey[setIdx]
|
||||
maskKeyVal := u.MaskKey[idx]
|
||||
setVal := paddedPasscodeSets[idx]
|
||||
cipheredMask[idx] = setKeyVal ^ maskKeyVal ^ setVal
|
||||
}
|
||||
mask := security.EncodeBase64Str(cipheredMask)
|
||||
return mask, nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) DecipherMask(mask string, setVals []uint64, passcodeLen int) ([]uint64, error) {
|
||||
decodedMask, err := security.DecodeBase64Str(mask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decipheredMask, err := security.XorLists(decodedMask, u.MaskKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKeyRandComponent, err := security.XorLists(setVals, u.SetKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passcodeSet := make([]uint64, passcodeLen)
|
||||
for idx, setCipher := range decipheredMask[:passcodeLen] {
|
||||
setIdx, err := security.IndexOf(setKeyRandComponent, setCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passcodeSet[idx] = setVals[setIdx]
|
||||
}
|
||||
return passcodeSet, nil
|
||||
}
|
||||
|
||||
func (u *UserCipherKeys) EncipherNKode(passcodeAttrIdx []int, customerAttrs CustomerAttributes) (*EncipheredNKode, error) {
|
||||
attrVals, err := customerAttrs.AttrValsForKp(*u.Kp)
|
||||
code, err := u.EncipherSaltHashCode(passcodeAttrIdx, attrVals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passcodeSet := make([]uint64, len(passcodeAttrIdx))
|
||||
|
||||
for idx := range passcodeSet {
|
||||
passcodeAttr := attrVals[passcodeAttrIdx[idx]]
|
||||
passcodeSet[idx], err = customerAttrs.GetAttrSetVal(passcodeAttr, *u.Kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
mask, err := u.EncipherMask(passcodeSet, customerAttrs, *u.Kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encipheredCode := EncipheredNKode{
|
||||
Code: code,
|
||||
Mask: mask,
|
||||
}
|
||||
return &encipheredCode, nil
|
||||
}
|
||||
194
internal/models/user_interface.go
Normal file
194
internal/models/user_interface.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/security"
|
||||
"go-nkode/internal/utils"
|
||||
"log"
|
||||
)
|
||||
|
||||
type UserInterface struct {
|
||||
IdxInterface IdxInterface
|
||||
SvgId SvgIdInterface
|
||||
Kp *KeypadDimension
|
||||
}
|
||||
|
||||
func NewUserInterface(kp *KeypadDimension, svgId SvgIdInterface) (*UserInterface, error) {
|
||||
idxInterface := security.IdentityArray(kp.TotalAttrs())
|
||||
userInterface := UserInterface{
|
||||
IdxInterface: idxInterface,
|
||||
SvgId: svgId,
|
||||
Kp: kp,
|
||||
}
|
||||
if err := userInterface.RandomShuffle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userInterface, nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) RandomShuffle() error {
|
||||
err := u.shuffleKeys()
|
||||
|
||||
keypadView, err := u.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setView, err := security.MatrixTranspose(keypadView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx, set := range setView {
|
||||
err := security.FisherYatesShuffle(&set)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
setView[idx] = set
|
||||
}
|
||||
|
||||
keypadView, err = security.MatrixTranspose(setView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.IdxInterface = security.MatrixToList(keypadView)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) InterfaceMatrix() ([][]int, error) {
|
||||
return security.ListToMatrix(u.IdxInterface, u.Kp.AttrsPerKey)
|
||||
}
|
||||
|
||||
func (u *UserInterface) SetViewMatrix() ([][]int, error) {
|
||||
keypadView, err := u.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return security.MatrixTranspose(keypadView)
|
||||
}
|
||||
|
||||
func (u *UserInterface) DisperseInterface() error {
|
||||
if !u.Kp.IsDispersable() {
|
||||
return config.ErrInterfaceNotDispersible
|
||||
}
|
||||
|
||||
err := u.shuffleKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = u.randomAttributeRotation()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) shuffleKeys() error {
|
||||
userInterfaceMatrix, err := security.ListToMatrix(u.IdxInterface, u.Kp.AttrsPerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = security.FisherYatesShuffle[[]int](&userInterfaceMatrix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.IdxInterface = security.MatrixToList(userInterfaceMatrix)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) randomAttributeRotation() error {
|
||||
userInterface, err := u.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transposeUserInterface, err := security.MatrixTranspose(userInterface)
|
||||
|
||||
attrRotation, err := security.RandomPermutation(len(transposeUserInterface))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, attrSet := range transposeUserInterface {
|
||||
rotation := attrRotation[idx]
|
||||
transposeUserInterface[idx] = append(attrSet[rotation:], attrSet[:rotation]...)
|
||||
}
|
||||
userInterface, err = security.MatrixTranspose(transposeUserInterface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.IdxInterface = security.MatrixToList(userInterface)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) AttributeAdjacencyGraph() (map[int]utils.Set[int], error) {
|
||||
interfaceKeypad, err := u.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph := make(map[int]utils.Set[int])
|
||||
|
||||
for _, key := range interfaceKeypad {
|
||||
keySet := utils.NewSetFromSlice(key)
|
||||
for _, attr := range key {
|
||||
attrAdjacency := keySet.Copy()
|
||||
attrAdjacency.Remove(attr)
|
||||
graph[attr] = attrAdjacency
|
||||
}
|
||||
}
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) PartialInterfaceShuffle() error {
|
||||
err := u.shuffleKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
numbOfSelectedSets := u.Kp.AttrsPerKey / 2
|
||||
if u.Kp.AttrsPerKey&1 == 1 {
|
||||
numbOfSelectedSets += security.Choice[int]([]int{0, 1})
|
||||
}
|
||||
setIdxs, err := security.RandomPermutation(u.Kp.AttrsPerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedSets := utils.NewSetFromSlice[int](setIdxs[:numbOfSelectedSets])
|
||||
|
||||
keypadSetView, err := u.SetViewMatrix()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
interfaceBySet := make([][]int, u.Kp.AttrsPerKey)
|
||||
for idx, attrs := range keypadSetView {
|
||||
if selectedSets.Contains(idx) {
|
||||
err = security.FisherYatesShuffle[int](&attrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
interfaceBySet[idx] = attrs
|
||||
}
|
||||
keypadView, err := security.MatrixTranspose(interfaceBySet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.IdxInterface = security.MatrixToList(keypadView)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) GetAttrIdxByKeyNumbSetIdx(setIdx int, keyNumb int) (int, error) {
|
||||
if keyNumb < 0 || u.Kp.NumbOfKeys <= keyNumb {
|
||||
log.Printf("keyNumb %d is out of range 0-%d", keyNumb, u.Kp.NumbOfKeys)
|
||||
return -1, config.ErrKeyIndexOutOfRange
|
||||
}
|
||||
|
||||
if setIdx < 0 || u.Kp.AttrsPerKey <= setIdx {
|
||||
log.Printf("setIdx %d is out of range 0-%d", setIdx, u.Kp.AttrsPerKey)
|
||||
return -1, config.ErrAttributeIndexOutOfRange
|
||||
}
|
||||
keypadView, err := u.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return keypadView[keyNumb][setIdx], nil
|
||||
}
|
||||
200
internal/models/user_signup_session.go
Normal file
200
internal/models/user_signup_session.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"go-nkode/config"
|
||||
"go-nkode/internal/security"
|
||||
py "go-nkode/internal/utils"
|
||||
"log"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type UserSignSession struct {
|
||||
Id SessionId
|
||||
CustomerId CustomerId
|
||||
LoginUserInterface UserInterface
|
||||
Kp KeypadDimension
|
||||
SetIdxInterface IdxInterface
|
||||
ConfirmIdxInterface IdxInterface
|
||||
SetKeySelection KeySelection
|
||||
UserEmail UserEmail
|
||||
Reset bool
|
||||
Expire int
|
||||
Colors []RGBColor
|
||||
}
|
||||
|
||||
func NewSignupResetSession(userEmail UserEmail, kp KeypadDimension, customerId CustomerId, svgInterface SvgIdInterface, reset bool) (*UserSignSession, error) {
|
||||
loginInterface, err := NewUserInterface(&kp, svgInterface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signupInterface, colors, err := signupInterface(*loginInterface, kp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session := UserSignSession{
|
||||
Id: SessionId(uuid.New()),
|
||||
CustomerId: customerId,
|
||||
LoginUserInterface: *loginInterface,
|
||||
SetIdxInterface: signupInterface.IdxInterface,
|
||||
ConfirmIdxInterface: nil,
|
||||
SetKeySelection: nil,
|
||||
UserEmail: userEmail,
|
||||
Kp: kp,
|
||||
Reset: reset,
|
||||
Colors: colors,
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (s *UserSignSession) DeducePasscode(confirmKeyEntry KeySelection) ([]int, error) {
|
||||
validEntry := py.All[int](confirmKeyEntry, func(i int) bool {
|
||||
return 0 <= i && i < s.Kp.NumbOfKeys
|
||||
})
|
||||
|
||||
if !validEntry {
|
||||
log.Printf("Invalid Key entry. One or more key index: %#v, not in range 0-%d", confirmKeyEntry, s.Kp.NumbOfKeys)
|
||||
return nil, config.ErrKeyIndexOutOfRange
|
||||
}
|
||||
|
||||
if s.SetIdxInterface == nil {
|
||||
log.Print("signup session set interface is nil")
|
||||
return nil, config.ErrIncompleteUserSignupSession
|
||||
}
|
||||
|
||||
if s.ConfirmIdxInterface == nil {
|
||||
log.Print("signup session confirm interface is nil")
|
||||
return nil, config.ErrIncompleteUserSignupSession
|
||||
}
|
||||
|
||||
if s.SetKeySelection == nil {
|
||||
log.Print("signup session set key entry is nil")
|
||||
return nil, config.ErrIncompleteUserSignupSession
|
||||
}
|
||||
|
||||
if s.UserEmail == "" {
|
||||
log.Print("signup session username is nil")
|
||||
return nil, config.ErrIncompleteUserSignupSession
|
||||
}
|
||||
|
||||
if len(confirmKeyEntry) != len(s.SetKeySelection) {
|
||||
log.Printf("confirm and set key entry length mismatch %d != %d", len(confirmKeyEntry), len(s.SetKeySelection))
|
||||
return nil, config.ErrSetConfirmSignupMismatch
|
||||
}
|
||||
|
||||
passcodeLen := len(confirmKeyEntry)
|
||||
setKeyVals, err := s.getSelectedKeyVals(s.SetKeySelection, s.SetIdxInterface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
confirmKeyVals, err := s.getSelectedKeyVals(confirmKeyEntry, s.ConfirmIdxInterface)
|
||||
passcode := make([]int, passcodeLen)
|
||||
|
||||
for idx := 0; idx < passcodeLen; idx++ {
|
||||
setKey := py.NewSetFromSlice[int](setKeyVals[idx])
|
||||
confirmKey := py.NewSetFromSlice[int](confirmKeyVals[idx])
|
||||
intersection := setKey.Intersect(confirmKey)
|
||||
if intersection.Size() < 1 {
|
||||
log.Printf("set and confirm do not intersect at index %d", idx)
|
||||
return nil, config.ErrSetConfirmSignupMismatch
|
||||
}
|
||||
if intersection.Size() > 1 {
|
||||
log.Printf("set and confirm intersect at more than one point at index %d", idx)
|
||||
return nil, config.ErrSetConfirmSignupMismatch
|
||||
}
|
||||
intersectionSlice := intersection.ToSlice()
|
||||
passcode[idx] = intersectionSlice[0]
|
||||
}
|
||||
return passcode, nil
|
||||
}
|
||||
|
||||
func (s *UserSignSession) SetUserNKode(keySelection KeySelection) (IdxInterface, error) {
|
||||
validKeySelection := py.All[int](keySelection, func(i int) bool {
|
||||
return 0 <= i && i < s.Kp.NumbOfKeys
|
||||
})
|
||||
if !validKeySelection {
|
||||
log.Printf("one or key selection is out of range 0-%d", s.Kp.NumbOfKeys-1)
|
||||
return nil, config.ErrKeyIndexOutOfRange
|
||||
}
|
||||
|
||||
s.SetKeySelection = keySelection
|
||||
setKp := s.SignupKeypad()
|
||||
setInterface := UserInterface{IdxInterface: s.SetIdxInterface, Kp: &setKp}
|
||||
err := setInterface.DisperseInterface()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.ConfirmIdxInterface = setInterface.IdxInterface
|
||||
return s.ConfirmIdxInterface, nil
|
||||
}
|
||||
|
||||
func (s *UserSignSession) getSelectedKeyVals(keySelections KeySelection, userInterface []int) ([][]int, error) {
|
||||
signupKp := s.SignupKeypad()
|
||||
keypadInterface, err := security.ListToMatrix(userInterface, signupKp.AttrsPerKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyVals := make([][]int, len(keySelections))
|
||||
|
||||
for idx, keyIdx := range keySelections {
|
||||
keyVals[idx] = keypadInterface[keyIdx]
|
||||
}
|
||||
return keyVals, nil
|
||||
}
|
||||
|
||||
func signupInterface(baseUserInterface UserInterface, kp KeypadDimension) (*UserInterface, []RGBColor, error) {
|
||||
// This method randomly drops sets from the base user interface so it is a square and dispersable matrix
|
||||
if kp.IsDispersable() {
|
||||
return nil, nil, config.ErrKeypadIsNotDispersible
|
||||
}
|
||||
err := baseUserInterface.RandomShuffle()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// attributes are arranged by key interfaceMatrix
|
||||
interfaceMatrix, err := baseUserInterface.InterfaceMatrix()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// attributes are arranged by set
|
||||
attrSetView, err := security.MatrixTranspose(interfaceMatrix)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
setIdxs := security.IdentityArray(kp.AttrsPerKey)
|
||||
if err := security.FisherYatesShuffle[int](&setIdxs); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
setIdxs = setIdxs[:kp.NumbOfKeys]
|
||||
sort.Ints(setIdxs)
|
||||
selectedSets := make([][]int, kp.NumbOfKeys)
|
||||
selectedColors := make([]RGBColor, kp.NumbOfKeys)
|
||||
|
||||
for idx, setIdx := range setIdxs {
|
||||
selectedSets[idx] = attrSetView[setIdx]
|
||||
selectedColors[idx] = SetColors[setIdx]
|
||||
}
|
||||
// convert set view back into key view
|
||||
selectedSets, err = security.MatrixTranspose(selectedSets)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
signupUserInterface := UserInterface{
|
||||
IdxInterface: security.MatrixToList(selectedSets),
|
||||
Kp: &KeypadDimension{
|
||||
AttrsPerKey: kp.NumbOfKeys,
|
||||
NumbOfKeys: kp.NumbOfKeys,
|
||||
},
|
||||
}
|
||||
return &signupUserInterface, selectedColors, nil
|
||||
}
|
||||
|
||||
func (s *UserSignSession) SignupKeypad() KeypadDimension {
|
||||
return KeypadDimension{
|
||||
AttrsPerKey: s.Kp.NumbOfKeys,
|
||||
NumbOfKeys: s.Kp.NumbOfKeys,
|
||||
}
|
||||
}
|
||||
133
internal/models/user_test.go
Normal file
133
internal/models/user_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
py "go-nkode/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserCipherKeys_EncipherSaltHashCode(t *testing.T) {
|
||||
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 8}
|
||||
maxNKodeLen := 10
|
||||
customerAttrs, err := NewCustomerAttributes()
|
||||
assert.NoError(t, err)
|
||||
setVals, err := customerAttrs.SetValsForKp(kp)
|
||||
assert.NoError(t, err)
|
||||
attrVals, err := customerAttrs.AttrValsForKp(kp)
|
||||
assert.NoError(t, err)
|
||||
newUser, err := NewUserCipherKeys(&kp, setVals, maxNKodeLen)
|
||||
assert.NoError(t, err)
|
||||
passcodeIdx := []int{0, 1, 2, 3}
|
||||
encipher0, err := newUser.EncipherSaltHashCode(passcodeIdx, attrVals)
|
||||
assert.NoError(t, err)
|
||||
err = newUser.ValidPassword(encipher0, passcodeIdx, attrVals)
|
||||
assert.NoError(t, err)
|
||||
|
||||
passcodeIdxInvalid := []int{1, 0, 3, 2}
|
||||
assert.NoError(t, err)
|
||||
err = newUser.ValidPassword(encipher0, passcodeIdxInvalid, attrVals)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUserCipherKeys_EncipherDecipherMask(t *testing.T) {
|
||||
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 8}
|
||||
maxNKodeLen := 10
|
||||
|
||||
customerAttrs, err := NewCustomerAttributes()
|
||||
assert.NoError(t, err)
|
||||
setVals, err := customerAttrs.SetValsForKp(kp)
|
||||
assert.NoError(t, err)
|
||||
attrVals, err := customerAttrs.AttrValsForKp(kp)
|
||||
assert.NoError(t, err)
|
||||
newUser, err := NewUserCipherKeys(&kp, setVals, maxNKodeLen)
|
||||
assert.NoError(t, err)
|
||||
passcodeIdx := []int{0, 1, 2, 3}
|
||||
originalSetVals := make([]uint64, len(passcodeIdx))
|
||||
|
||||
for idx, val := range passcodeIdx {
|
||||
attr := attrVals[val]
|
||||
originalSetVals[idx], err = customerAttrs.GetAttrSetVal(attr, kp)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
encipheredCode, err := newUser.EncipherNKode(passcodeIdx, *customerAttrs)
|
||||
assert.NoError(t, err)
|
||||
passcodeSetVals, err := newUser.DecipherMask(encipheredCode.Mask, setVals, len(passcodeIdx))
|
||||
assert.NoError(t, err)
|
||||
|
||||
for idx, setVal := range passcodeSetVals {
|
||||
assert.Equal(t, setVal, originalSetVals[idx])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInterface_RandomShuffle(t *testing.T) {
|
||||
kp := KeypadDimension{
|
||||
AttrsPerKey: 10,
|
||||
NumbOfKeys: 8,
|
||||
}
|
||||
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
|
||||
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
userInterfaceCopy := make([]int, len(userInterface.IdxInterface))
|
||||
copy(userInterfaceCopy, userInterface.IdxInterface)
|
||||
|
||||
err = userInterface.RandomShuffle()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(userInterface.IdxInterface), len(userInterfaceCopy))
|
||||
equalCount := 0
|
||||
for idx, val := range userInterface.IdxInterface {
|
||||
if val == userInterfaceCopy[idx] {
|
||||
equalCount++
|
||||
}
|
||||
}
|
||||
assert.NotEqual(t, equalCount, len(userInterface.IdxInterface))
|
||||
}
|
||||
|
||||
func TestUserInterface_DisperseInterface(t *testing.T) {
|
||||
|
||||
for idx := 0; idx < 10000; idx++ {
|
||||
kp := KeypadDimension{AttrsPerKey: 7, NumbOfKeys: 10}
|
||||
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
|
||||
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
preDispersion, err := userInterface.AttributeAdjacencyGraph()
|
||||
assert.NoError(t, err)
|
||||
err = userInterface.DisperseInterface()
|
||||
assert.NoError(t, err)
|
||||
postDispersion, err := userInterface.AttributeAdjacencyGraph()
|
||||
assert.Equal(t, len(postDispersion), len(preDispersion))
|
||||
for attr, adjAttrs := range preDispersion {
|
||||
postAdjAttrs := postDispersion[attr]
|
||||
assert.Equal(t, adjAttrs.Size(), postAdjAttrs.Size())
|
||||
assert.True(t, adjAttrs.IsDisjoint(postAdjAttrs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInterface_PartialInterfaceShuffle(t *testing.T) {
|
||||
kp := KeypadDimension{AttrsPerKey: 7, NumbOfKeys: 10}
|
||||
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
|
||||
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
|
||||
assert.NoError(t, err)
|
||||
preShuffle := userInterface.IdxInterface
|
||||
err = userInterface.PartialInterfaceShuffle()
|
||||
assert.NoError(t, err)
|
||||
postShuffle := userInterface.IdxInterface
|
||||
|
||||
shuffleCompare := make([]bool, len(postShuffle))
|
||||
for idx, val := range preShuffle {
|
||||
shuffleCompare[idx] = val == postShuffle[idx]
|
||||
}
|
||||
|
||||
allTrue := py.All[bool](shuffleCompare, func(n bool) bool {
|
||||
return n == true
|
||||
})
|
||||
assert.False(t, allTrue)
|
||||
|
||||
allFalse := py.All[bool](shuffleCompare, func(n bool) bool {
|
||||
return n == false
|
||||
})
|
||||
|
||||
assert.False(t, allFalse)
|
||||
|
||||
}
|
||||
123
internal/security/jwt_claims.go
Normal file
123
internal/security/jwt_claims.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"go-nkode/config"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthenticationTokens struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type ResetNKodeClaims struct {
|
||||
Reset bool `json:"reset"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
const (
|
||||
accessTokenExp = 5 * time.Minute
|
||||
refreshTokenExp = 30 * 24 * time.Hour
|
||||
resetNKodeTokenExp = 5 * time.Minute
|
||||
)
|
||||
|
||||
var secret = getJwtSecret()
|
||||
|
||||
func getJwtSecret() []byte {
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
log.Fatal("No JWT_SECRET found")
|
||||
}
|
||||
|
||||
jwtBytes, err := ParseHexString(jwtSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("error parsing jwt secret %v", err)
|
||||
}
|
||||
return jwtBytes
|
||||
}
|
||||
|
||||
func NewAuthenticationTokens(username string, customerId uuid.UUID) (AuthenticationTokens, error) {
|
||||
accessClaims := NewAccessClaim(username, customerId)
|
||||
|
||||
refreshClaims := jwt.RegisteredClaims{
|
||||
Subject: username,
|
||||
Issuer: customerId.String(),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(refreshTokenExp)),
|
||||
}
|
||||
|
||||
accessJwt, err := EncodeAndSignClaims(accessClaims)
|
||||
if err != nil {
|
||||
return AuthenticationTokens{}, err
|
||||
}
|
||||
refreshJwt, err := EncodeAndSignClaims(refreshClaims)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationTokens{}, err
|
||||
}
|
||||
return AuthenticationTokens{
|
||||
AccessToken: accessJwt,
|
||||
RefreshToken: refreshJwt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAccessClaim(username string, customerId uuid.UUID) jwt.RegisteredClaims {
|
||||
return jwt.RegisteredClaims{
|
||||
Subject: username,
|
||||
Issuer: customerId.String(),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessTokenExp)),
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeAndSignClaims(claims jwt.Claims) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
|
||||
func ParseRegisteredClaimToken(token string) (*jwt.RegisteredClaims, error) {
|
||||
return parseJwt[*jwt.RegisteredClaims](token, &jwt.RegisteredClaims{})
|
||||
}
|
||||
|
||||
func ParseRestNKodeToken(resetNKodeToken string) (*ResetNKodeClaims, error) {
|
||||
return parseJwt[*ResetNKodeClaims](resetNKodeToken, &ResetNKodeClaims{})
|
||||
}
|
||||
|
||||
func parseJwt[T *ResetNKodeClaims | *jwt.RegisteredClaims](tokenStr string, claim jwt.Claims) (T, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, claim, func(token *jwt.Token) (interface{}, error) {
|
||||
return secret, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error parsing refresh token: %v", err)
|
||||
return nil, config.ErrInvalidJwt
|
||||
}
|
||||
claims, ok := token.Claims.(T)
|
||||
if !ok {
|
||||
return nil, config.ErrInvalidJwt
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func ClaimExpired(claims jwt.RegisteredClaims) error {
|
||||
if claims.ExpiresAt == nil {
|
||||
return config.ErrClaimExpOrNil
|
||||
}
|
||||
if claims.ExpiresAt.Time.After(time.Now()) {
|
||||
return nil
|
||||
}
|
||||
return config.ErrClaimExpOrNil
|
||||
}
|
||||
|
||||
func ResetNKodeToken(userEmail string, customerId uuid.UUID) (string, error) {
|
||||
resetClaims := ResetNKodeClaims{
|
||||
true,
|
||||
jwt.RegisteredClaims{
|
||||
Subject: userEmail,
|
||||
Issuer: customerId.String(),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(resetNKodeTokenExp)),
|
||||
},
|
||||
}
|
||||
return EncodeAndSignClaims(resetClaims)
|
||||
}
|
||||
28
internal/security/jwt_claims_test.go
Normal file
28
internal/security/jwt_claims_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJwtClaims(t *testing.T) {
|
||||
email := "testing@example.com"
|
||||
customerId := uuid.New()
|
||||
authTokens, err := NewAuthenticationTokens(email, customerId)
|
||||
assert.NoError(t, err)
|
||||
accessToken, err := ParseRegisteredClaimToken(authTokens.AccessToken)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accessToken.Subject, email)
|
||||
assert.NoError(t, ClaimExpired(*accessToken))
|
||||
refreshToken, err := ParseRegisteredClaimToken(authTokens.RefreshToken)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, refreshToken.Subject, email)
|
||||
assert.NoError(t, ClaimExpired(*refreshToken))
|
||||
resetNKode, err := ResetNKodeToken(email, customerId)
|
||||
assert.NoError(t, err)
|
||||
resetToken, err := ParseRestNKodeToken(resetNKode)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, resetToken.Reset)
|
||||
assert.Equal(t, resetToken.Subject, email)
|
||||
}
|
||||
289
internal/security/util.go
Normal file
289
internal/security/util.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"go-nkode/internal/utils"
|
||||
"log"
|
||||
"math/big"
|
||||
r "math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFisherYatesShuffle = errors.New("unable to shuffle array")
|
||||
ErrRandomBytes = errors.New("random bytes error")
|
||||
ErrRandNonRepeatingUint64 = errors.New("list length must be less than 2^32")
|
||||
ErrParseHexString = errors.New("parse hex string error")
|
||||
ErrMatrixTranspose = errors.New("matrix cannot be nil or empty")
|
||||
ErrListToMatrixNotDivisible = errors.New("list to matrix not possible")
|
||||
ErrElementNotInArray = errors.New("element not in array")
|
||||
ErrDecodeBase64Str = errors.New("decode base64 err")
|
||||
ErrRandNonRepeatingInt = errors.New("list length must be less than 2^31")
|
||||
ErrXorLengthMismatch = errors.New("xor length mismatch")
|
||||
)
|
||||
|
||||
func fisherYatesShuffle[T any](b *[]T) error {
|
||||
for i := len(*b) - 1; i > 0; i-- {
|
||||
bigJ, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
||||
if err != nil {
|
||||
log.Print("fisher yates shuffle error: ", err)
|
||||
return ErrFisherYatesShuffle
|
||||
}
|
||||
j := bigJ.Int64()
|
||||
(*b)[i], (*b)[j] = (*b)[j], (*b)[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FisherYatesShuffle[T any](b *[]T) error {
|
||||
return fisherYatesShuffle(b)
|
||||
}
|
||||
|
||||
func RandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Print("error in random bytes: ", err)
|
||||
return nil, ErrRandomBytes
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func RandomPermutation(n int) ([]int, error) {
|
||||
perm := IdentityArray(n)
|
||||
err := fisherYatesShuffle(&perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
func GenerateRandomUInt64() (uint64, error) {
|
||||
randBytes, err := RandomBytes(8)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
val := binary.LittleEndian.Uint64(randBytes)
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func GenerateRandomInt() (int, error) {
|
||||
randBytes, err := RandomBytes(8)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
val := int(binary.LittleEndian.Uint64(randBytes) & 0x7FFFFFFFFFFFFFFF) // Ensure it's positive
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func GenerateRandomNonRepeatingUint64(listLen int) ([]uint64, error) {
|
||||
if listLen > int(1)<<32 {
|
||||
return nil, ErrRandNonRepeatingUint64
|
||||
}
|
||||
listSet := make(utils.Set[uint64])
|
||||
for {
|
||||
if listSet.Size() == listLen {
|
||||
break
|
||||
}
|
||||
randNum, err := GenerateRandomUInt64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listSet.Add(randNum)
|
||||
}
|
||||
|
||||
data := listSet.ToSlice()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GenerateRandomNonRepeatingInt(listLen int) ([]int, error) {
|
||||
if listLen > int(1)<<31 {
|
||||
return nil, ErrRandNonRepeatingInt
|
||||
}
|
||||
listSet := make(utils.Set[int])
|
||||
for {
|
||||
if listSet.Size() == listLen {
|
||||
break
|
||||
}
|
||||
randNum, err := GenerateRandomInt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listSet.Add(randNum)
|
||||
}
|
||||
|
||||
data := listSet.ToSlice()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func XorLists(l0 []uint64, l1 []uint64) ([]uint64, error) {
|
||||
if len(l0) != len(l1) {
|
||||
log.Printf("list len mismatch %d, %d", len(l0), len(l1))
|
||||
return nil, ErrXorLengthMismatch
|
||||
}
|
||||
|
||||
xorList := make([]uint64, len(l0))
|
||||
for idx := 0; idx < len(l0); idx++ {
|
||||
xorList[idx] = l0[idx] ^ l1[idx]
|
||||
}
|
||||
return xorList, nil
|
||||
}
|
||||
|
||||
func EncodeBase64Str(data []uint64) string {
|
||||
dataBytes := Uint64ArrToByteArr(data)
|
||||
encoded := base64.StdEncoding.EncodeToString(dataBytes)
|
||||
return encoded
|
||||
}
|
||||
|
||||
func DecodeBase64Str(encoded string) ([]uint64, error) {
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
log.Print("error decoding base64 str: ", err)
|
||||
return nil, ErrDecodeBase64Str
|
||||
}
|
||||
data := ByteArrToUint64Arr(dataBytes)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func Uint64ArrToByteArr(intArr []uint64) []byte {
|
||||
byteArr := make([]byte, len(intArr)*8)
|
||||
for idx, val := range intArr {
|
||||
startIdx := idx * 8
|
||||
endIdx := (idx + 1) * 8
|
||||
binary.LittleEndian.PutUint64(byteArr[startIdx:endIdx], val)
|
||||
}
|
||||
return byteArr
|
||||
}
|
||||
|
||||
func IntArrToByteArr(intArr []int) []byte {
|
||||
byteArr := make([]byte, len(intArr)*4)
|
||||
for idx, val := range intArr {
|
||||
uval := uint32(val)
|
||||
startIdx := idx * 4
|
||||
endIdx := (idx + 1) * 4
|
||||
binary.LittleEndian.PutUint32(byteArr[startIdx:endIdx], uval)
|
||||
}
|
||||
return byteArr
|
||||
}
|
||||
|
||||
func ByteArrToUint64Arr(byteArr []byte) []uint64 {
|
||||
intArr := make([]uint64, len(byteArr)/8)
|
||||
for idx := 0; idx < len(intArr); idx++ {
|
||||
startIdx := idx * 8
|
||||
endIdx := (idx + 1) * 8
|
||||
intArr[idx] = binary.LittleEndian.Uint64(byteArr[startIdx:endIdx])
|
||||
}
|
||||
return intArr
|
||||
}
|
||||
|
||||
func ByteArrToIntArr(byteArr []byte) []int {
|
||||
intArr := make([]int, len(byteArr)/4)
|
||||
for idx := 0; idx < len(intArr); idx++ {
|
||||
startIdx := idx * 4
|
||||
endIdx := (idx + 1) * 4
|
||||
uval := binary.LittleEndian.Uint32(byteArr[startIdx:endIdx])
|
||||
intArr[idx] = int(uval)
|
||||
}
|
||||
return intArr
|
||||
}
|
||||
|
||||
func IndexOf[T uint64 | int](arr []T, el T) (int, error) {
|
||||
for idx, val := range arr {
|
||||
if val == el {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return -1, ErrElementNotInArray
|
||||
}
|
||||
|
||||
func IdentityArray(arrLen int) []int {
|
||||
identityArr := make([]int, arrLen)
|
||||
|
||||
for idx := range identityArr {
|
||||
identityArr[idx] = idx
|
||||
}
|
||||
return identityArr
|
||||
}
|
||||
|
||||
func ListToMatrix(listArr []int, numbCols int) ([][]int, error) {
|
||||
if len(listArr)%numbCols != 0 {
|
||||
log.Printf("Array is not evenly divisible by number of columns: %d mod %d = %d", len(listArr), numbCols, len(listArr)%numbCols)
|
||||
return nil, ErrListToMatrixNotDivisible
|
||||
}
|
||||
numbRows := len(listArr) / numbCols
|
||||
matrix := make([][]int, numbRows)
|
||||
for idx := range matrix {
|
||||
startIdx := idx * numbCols
|
||||
endIdx := (idx + 1) * numbCols
|
||||
matrix[idx] = listArr[startIdx:endIdx]
|
||||
}
|
||||
return matrix, nil
|
||||
}
|
||||
|
||||
func MatrixTranspose(matrix [][]int) ([][]int, error) {
|
||||
if matrix == nil || len(matrix) == 0 {
|
||||
log.Print("can't transpose nil or zero len matrix")
|
||||
return nil, ErrMatrixTranspose
|
||||
}
|
||||
|
||||
rows := len(matrix)
|
||||
cols := len((matrix)[0])
|
||||
|
||||
// Check if the matrix is not rectangular
|
||||
for _, row := range matrix {
|
||||
if len(row) != cols {
|
||||
log.Print("all rows must have the same number of columns")
|
||||
return nil, ErrMatrixTranspose
|
||||
}
|
||||
}
|
||||
|
||||
transposed := make([][]int, cols)
|
||||
for i := range transposed {
|
||||
transposed[i] = make([]int, rows)
|
||||
}
|
||||
|
||||
for i := 0; i < rows; i++ {
|
||||
for j := 0; j < cols; j++ {
|
||||
transposed[j][i] = (matrix)[i][j]
|
||||
}
|
||||
}
|
||||
|
||||
return transposed, nil
|
||||
}
|
||||
|
||||
func MatrixToList(matrix [][]int) []int {
|
||||
var flat []int
|
||||
for _, row := range matrix {
|
||||
flat = append(flat, row...)
|
||||
}
|
||||
return flat
|
||||
}
|
||||
|
||||
func Choice[T any](items []T) T {
|
||||
r.Seed(time.Now().UnixNano()) // Seed the random number generator
|
||||
return items[r.Intn(len(items))]
|
||||
}
|
||||
|
||||
// GenerateRandomString creates a random string of a specified length.
|
||||
func GenerateRandomString(length int) string {
|
||||
charset := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
b := make([]rune, length)
|
||||
for i := range b {
|
||||
b[i] = Choice[rune](charset)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ParseHexString(hexStr string) ([]byte, error) {
|
||||
// Decode the hex string into bytes
|
||||
bytes, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
log.Print("parse hex string err: ", err)
|
||||
return nil, ErrParseHexString
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
62
internal/security/util_test.go
Normal file
62
internal/security/util_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRandomNonRepeatingUint64(t *testing.T) {
|
||||
arrLen := 100000
|
||||
randNumbs, err := GenerateRandomNonRepeatingUint64(arrLen)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(randNumbs), arrLen)
|
||||
}
|
||||
|
||||
func TestGenerateRandomNonRepeatingInt(t *testing.T) {
|
||||
arrLen := 100000
|
||||
randNumbs, err := GenerateRandomNonRepeatingInt(arrLen)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(randNumbs), arrLen)
|
||||
}
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
testArr := []uint64{1, 2, 3, 4, 5, 6}
|
||||
testEncode := EncodeBase64Str(testArr)
|
||||
testDecode, err := DecodeBase64Str(testEncode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(testArr), len(testDecode))
|
||||
for idx, val := range testArr {
|
||||
assert.Equal(t, val, testDecode[idx])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatrixTranspose(t *testing.T) {
|
||||
matrix := [][]int{
|
||||
{0, 1, 2},
|
||||
{3, 4, 5},
|
||||
}
|
||||
expectedMatrixT := [][]int{
|
||||
{0, 3},
|
||||
{1, 4},
|
||||
{2, 5},
|
||||
}
|
||||
expectedFlatMat := MatrixToList(expectedMatrixT)
|
||||
matrixT, err := MatrixTranspose(matrix)
|
||||
assert.NoError(t, err)
|
||||
flatMat := MatrixToList(matrixT)
|
||||
|
||||
assert.Equal(t, len(flatMat), len(expectedFlatMat))
|
||||
for idx := range flatMat {
|
||||
assert.Equal(t, expectedFlatMat[idx], flatMat[idx])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntToByteAndBack(t *testing.T) {
|
||||
origIntArr := []int{1, 2, 3, 4, 5}
|
||||
byteArr := IntArrToByteArr(origIntArr)
|
||||
intArr := ByteArrToIntArr(byteArr)
|
||||
|
||||
assert.ElementsMatch(t, origIntArr, intArr)
|
||||
}
|
||||
63
internal/utils/hashset.go
Normal file
63
internal/utils/hashset.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package utils
|
||||
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
func (s *Set[T]) Add(element T) {
|
||||
(*s)[element] = struct{}{}
|
||||
}
|
||||
|
||||
func (s *Set[T]) Remove(element T) {
|
||||
delete(*s, element)
|
||||
}
|
||||
|
||||
func (s *Set[T]) Contains(element T) bool {
|
||||
_, exists := (*s)[element]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s *Set[T]) Size() int {
|
||||
return len(*s)
|
||||
}
|
||||
|
||||
func (s *Set[T]) ToSlice() []T {
|
||||
list := make([]T, 0, len(*s))
|
||||
for key := range *s {
|
||||
list = append(list, key)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func NewSetFromSlice[T comparable](slice []T) Set[T] {
|
||||
set := make(Set[T])
|
||||
for _, val := range slice {
|
||||
set.Add(val)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (s *Set[T]) Copy() Set[T] {
|
||||
newSet := make(Set[T])
|
||||
for key, val := range *s {
|
||||
newSet[key] = val
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
func (s *Set[T]) IsDisjoint(otherSet Set[T]) bool {
|
||||
for attr := range *s {
|
||||
if otherSet.Contains(attr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Set[T]) Intersect(otherSet Set[T]) Set[T] {
|
||||
intersect := make(Set[T])
|
||||
for val := range *s {
|
||||
if otherSet.Contains(val) {
|
||||
intersect.Add(val)
|
||||
}
|
||||
}
|
||||
return intersect
|
||||
}
|
||||
35
internal/utils/hashset_test.go
Normal file
35
internal/utils/hashset_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
intSet := make(Set[int])
|
||||
intSet.Add(1)
|
||||
intSet.Add(2)
|
||||
assert.EqualValues(t, intSet.Size(), 2)
|
||||
intSet.Add(3)
|
||||
intSet.Add(3)
|
||||
assert.EqualValues(t, intSet.Size(), 3)
|
||||
intSet.Remove(2)
|
||||
assert.EqualValues(t, intSet.Size(), 2)
|
||||
assert.False(t, intSet.Contains(2))
|
||||
assert.True(t, intSet.Contains(1))
|
||||
|
||||
list := intSet.ToSlice()
|
||||
assert.Contains(t, list, 1)
|
||||
assert.Contains(t, list, 3)
|
||||
}
|
||||
|
||||
func TestSet_Copy(t *testing.T) {
|
||||
intSet := NewSetFromSlice[int]([]int{1, 2, 3})
|
||||
|
||||
copySet := intSet.Copy()
|
||||
|
||||
intSet.Remove(1)
|
||||
assert.Equal(t, intSet.Size(), 2)
|
||||
assert.Equal(t, copySet.Size(), 3)
|
||||
|
||||
}
|
||||
11
internal/utils/py-builtin.go
Normal file
11
internal/utils/py-builtin.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
func All[T comparable](slice []T, condition func(T) bool) bool {
|
||||
|
||||
for _, v := range slice {
|
||||
if !condition(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user