add apparmor hat transitions
This commit is contained in:
parent
d8ec9bb883
commit
0fb7171198
14 changed files with 336 additions and 33 deletions
16
.vscode/c_cpp_properties.json
vendored
Normal file
16
.vscode/c_cpp_properties.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Linux",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
],
|
||||||
|
"defines": [],
|
||||||
|
"compilerPath": "/usr/bin/gcc",
|
||||||
|
"cStandard": "gnu17",
|
||||||
|
"cppStandard": "gnu++17",
|
||||||
|
"intelliSenseMode": "linux-gcc-x64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<h2>お姫様の成績がアップデートされました!</h2>
|
<h2>お姫様の成績がアップデートされました!</h2>
|
||||||
<h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</h2>
|
<h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</a></h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -17,10 +17,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ .SubmissionUserName }}</td>
|
<td>{{ .SubmissionUserName }}</td>
|
||||||
<td>{{ .CourseCode }}</td>
|
<td>{{ .CourseCode }}</td>
|
||||||
<td><a href="{{.AssignmentURL}}"">{{ .Name }}</a></td>
|
<td><a href="{{.AssignmentURL}}">{{ .Name }}</a></td>
|
||||||
<td>{{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}</td>
|
<td>{{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}</td>
|
||||||
<td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f"
|
<td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" }} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
|
||||||
}} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
|
|
||||||
<td>{{ .GradedAt }} </td>
|
<td>{{ .GradedAt }} </td>
|
||||||
<td>{{ .PostedAt }}</td>
|
<td>{{ .PostedAt }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -3,8 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/eternal-flame-AD/yoake/config"
|
"github.com/eternal-flame-AD/yoake/config"
|
||||||
|
"github.com/eternal-flame-AD/yoake/internal/apparmor"
|
||||||
"github.com/eternal-flame-AD/yoake/internal/comm"
|
"github.com/eternal-flame-AD/yoake/internal/comm"
|
||||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||||
"github.com/eternal-flame-AD/yoake/server"
|
"github.com/eternal-flame-AD/yoake/server"
|
||||||
|
@ -37,10 +39,45 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func changeHat() {
|
||||||
|
profile := config.Config().Listen.AppArmor.Serve
|
||||||
|
if profile != "" {
|
||||||
|
token, err := apparmor.GetMagicToken()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("failed to get apparmor magic token: %v", err)
|
||||||
|
}
|
||||||
|
if err := apparmor.ChangeHat(profile, token); err != nil {
|
||||||
|
log.Panicf("failed to change apparmor hat: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("changed apparmor hat to %s", profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
listen := config.Config().Listen
|
listen := config.Config().Listen
|
||||||
if listen.Ssl.Use {
|
if listen.Ssl.Use {
|
||||||
log.Fatalln(server.Server.StartTLS(listen.Addr, listen.Ssl.Cert, listen.Ssl.Key))
|
var sslCertBytes, sslKeyBytes []byte
|
||||||
|
apparmor.ExecuteInHat(listen.AppArmor.SSL, func() {
|
||||||
|
var err error
|
||||||
|
sslCertBytes, err = os.ReadFile(listen.Ssl.Cert)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("failed to read ssl cert: %v", err)
|
||||||
|
}
|
||||||
|
sslKeyBytes, err = os.ReadFile(listen.Ssl.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("failed to read ssl key: %v", err)
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
if listen.AppArmor.SSL != "" {
|
||||||
|
// defensive programming, try read ssl key
|
||||||
|
if _, err := os.ReadFile(listen.Ssl.Key); err == nil {
|
||||||
|
log.Panicf("AppArmor profile set for SSL but I could still read %v!", listen.Ssl.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalln(server.Server.StartTLS(listen.Addr, sslCertBytes, sslKeyBytes))
|
||||||
} else {
|
} else {
|
||||||
log.Fatalln(server.Server.Start(listen.Addr))
|
log.Fatalln(server.Server.Start(listen.Addr))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
listen:
|
listen:
|
||||||
addr: :80
|
addr: :80
|
||||||
|
apparmor:
|
||||||
|
ssl: ssl
|
||||||
ssl:
|
ssl:
|
||||||
use: off
|
use: off
|
||||||
cert: "fullchain.pem"
|
cert: "fullchain.pem"
|
||||||
|
|
|
@ -8,8 +8,12 @@ import (
|
||||||
type C struct {
|
type C struct {
|
||||||
Hosts map[string]string
|
Hosts map[string]string
|
||||||
Listen struct {
|
Listen struct {
|
||||||
Addr string
|
Addr string
|
||||||
Ssl struct {
|
AppArmor struct {
|
||||||
|
Serve string
|
||||||
|
SSL string
|
||||||
|
}
|
||||||
|
Ssl struct {
|
||||||
Use bool
|
Use bool
|
||||||
Cert string
|
Cert string
|
||||||
Key string
|
Key string
|
||||||
|
|
|
@ -6,12 +6,13 @@ make verify
|
||||||
make build
|
make build
|
||||||
or exit 2
|
or exit 2
|
||||||
|
|
||||||
|
if ! diff etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
||||||
|
sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
||||||
|
or exit 2
|
||||||
|
|
||||||
sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
sudo systemctl daemon-reload
|
||||||
or exit 2
|
or exit 2
|
||||||
|
end
|
||||||
sudo systemctl daemon-reload
|
|
||||||
or exit 2
|
|
||||||
|
|
||||||
sudo systemctl stop yoake-server.service
|
sudo systemctl stop yoake-server.service
|
||||||
|
|
||||||
|
|
65
internal/apparmor/api.go
Normal file
65
internal/apparmor/api.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package apparmor
|
||||||
|
|
||||||
|
import (
|
||||||
|
// #cgo LDFLAGS: -lapparmor
|
||||||
|
// #include "./apparmor.h"
|
||||||
|
"C"
|
||||||
|
)
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChangeHat(subprofile string, magicToken uint64) error {
|
||||||
|
var ret uintptr
|
||||||
|
if subprofile != "" {
|
||||||
|
subProfileC := C.CString(subprofile)
|
||||||
|
defer C.free(unsafe.Pointer(subProfileC))
|
||||||
|
ret = uintptr(C.go_aa_change_hat(subProfileC, C.ulong(magicToken)))
|
||||||
|
} else {
|
||||||
|
ret = uintptr(C.go_aa_change_hat(nil, C.ulong(magicToken)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret != 0 {
|
||||||
|
return syscall.Errno(ret)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteInHat(subprofile string, fn func(), lockThread bool) error {
|
||||||
|
if subprofile == "" {
|
||||||
|
fn()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if lockThread {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
}
|
||||||
|
token, err := GetMagicToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ChangeHat(subprofile, token); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fn()
|
||||||
|
return ChangeHat("", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeProfile(subprofile string) error {
|
||||||
|
var ret uintptr
|
||||||
|
|
||||||
|
if subprofile != "" {
|
||||||
|
subProfileC := C.CString(subprofile)
|
||||||
|
defer C.free(unsafe.Pointer(subProfileC))
|
||||||
|
ret = uintptr(C.go_aa_change_profile(subProfileC))
|
||||||
|
} else {
|
||||||
|
ret = uintptr(C.go_aa_change_profile(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret != 0 {
|
||||||
|
return syscall.Errno(ret)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
21
internal/apparmor/apparmor.c
Normal file
21
internal/apparmor/apparmor.c
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#include "./apparmor.h"
|
||||||
|
|
||||||
|
int go_aa_change_hat(const char *hat, unsigned long magic)
|
||||||
|
{
|
||||||
|
int ret = aa_change_hat(hat, magic);
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int go_aa_change_profile(const char *profile)
|
||||||
|
{
|
||||||
|
int ret = aa_change_profile(profile);
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
8
internal/apparmor/apparmor.h
Normal file
8
internal/apparmor/apparmor.h
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/apparmor.h>
|
||||||
|
|
||||||
|
int go_aa_change_hat(const char *hat, unsigned long magic);
|
||||||
|
|
||||||
|
int go_aa_change_profile(const char *profile);
|
17
internal/apparmor/magic.go
Normal file
17
internal/apparmor/magic.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package apparmor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetMagicToken() (uint64, error) {
|
||||||
|
var buf [64 / 8]byte
|
||||||
|
if _, err := rand.Read(buf[:]); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to generate magic token: %v", err)
|
||||||
|
}
|
||||||
|
return uint64(buf[0])<<56 | uint64(buf[1])<<48 |
|
||||||
|
uint64(buf[2])<<40 | uint64(buf[3])<<32 |
|
||||||
|
uint64(buf[4])<<24 | uint64(buf[5])<<16 |
|
||||||
|
uint64(buf[6])<<8 | uint64(buf[7]), nil
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/alexedwards/argon2id"
|
"github.com/alexedwards/argon2id"
|
||||||
"github.com/eternal-flame-AD/yoake/config"
|
"github.com/eternal-flame-AD/yoake/config"
|
||||||
|
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
@ -16,6 +19,20 @@ import (
|
||||||
|
|
||||||
const AuthSessionName = "auth_session"
|
const AuthSessionName = "auth_session"
|
||||||
|
|
||||||
|
var dummyHash string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var dummyPassword [16]byte
|
||||||
|
_, err := rand.Read(dummyPassword[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dummyHash, err = argon2id.CreateHash(string(dummyPassword[:]), Argon2IdParams)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type RequestAuth struct {
|
type RequestAuth struct {
|
||||||
Present bool
|
Present bool
|
||||||
Valid bool
|
Valid bool
|
||||||
|
@ -118,6 +135,8 @@ func issueSession(c echo.Context, period time.Duration, roles []string) error {
|
||||||
sess.Options = &sessions.Options{
|
sess.Options = &sessions.Options{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
Secure: config.Config().Listen.Ssl.Use,
|
||||||
}
|
}
|
||||||
if period == 0 {
|
if period == 0 {
|
||||||
period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute
|
period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute
|
||||||
|
@ -139,6 +158,8 @@ type LoginForm struct {
|
||||||
OtpResponse string `json:"otp_response" form:"otp_response"`
|
OtpResponse string `json:"otp_response" form:"otp_response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errInvalidUserPass = echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid username or password"))
|
||||||
|
|
||||||
func Register(g *echo.Group) (err error) {
|
func Register(g *echo.Group) (err error) {
|
||||||
g.GET("/auth.json", func(c echo.Context) error {
|
g.GET("/auth.json", func(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, GetRequestAuth(c))
|
return c.JSON(http.StatusOK, GetRequestAuth(c))
|
||||||
|
@ -181,30 +202,31 @@ func Register(g *echo.Group) (err error) {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "password required")
|
return echo.NewHTTPError(http.StatusBadRequest, "password required")
|
||||||
}
|
}
|
||||||
if user, ok := config.Config().Auth.Users[form.Username]; ok {
|
if user, ok := config.Config().Auth.Users[form.Username]; ok {
|
||||||
if len(user.PublicKeyId) > 0 {
|
|
||||||
if verifiedOtpPubId == "" {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "otp required")
|
|
||||||
}
|
|
||||||
found := 0
|
|
||||||
for _, pubId := range user.PublicKeyId {
|
|
||||||
found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId))
|
|
||||||
}
|
|
||||||
if found == 0 {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used")
|
|
||||||
}
|
|
||||||
} else if verifiedOtpPubId != "" {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match {
|
if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match {
|
||||||
|
if len(user.PublicKeyId) > 0 {
|
||||||
|
if verifiedOtpPubId == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "otp required")
|
||||||
|
}
|
||||||
|
found := 0
|
||||||
|
for _, pubId := range user.PublicKeyId {
|
||||||
|
found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId))
|
||||||
|
}
|
||||||
|
if found == 0 {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used")
|
||||||
|
}
|
||||||
|
} else if verifiedOtpPubId != "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
|
||||||
|
}
|
||||||
|
|
||||||
issueSession(c, 0, user.Roles)
|
issueSession(c, 0, user.Roles)
|
||||||
c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true})
|
c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true})
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect password")
|
return errInvalidUserPass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "invalid username")
|
argon2id.ComparePasswordAndHash(form.Password, dummyHash)
|
||||||
|
return errInvalidUserPass
|
||||||
}, loginRateLimiter)
|
}, loginRateLimiter)
|
||||||
g.DELETE("/login", func(c echo.Context) error {
|
g.DELETE("/login", func(c echo.Context) error {
|
||||||
return issueSession(c, -1, nil)
|
return issueSession(c, -1, nil)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package utilapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexedwards/argon2id"
|
"github.com/alexedwards/argon2id"
|
||||||
|
@ -36,6 +37,12 @@ func Register(g *echo.Group) (err error) {
|
||||||
Store: limiterStore,
|
Store: limiterStore,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
g.GET("/tryopen", func(c echo.Context) error {
|
||||||
|
if _, err := os.ReadFile(c.QueryParam("path")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.String(200, c.QueryParam("path"))
|
||||||
|
}, auth.RequireMiddleware(auth.RoleAdmin))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package server
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/eternal-flame-AD/yoake/config"
|
||||||
|
"github.com/eternal-flame-AD/yoake/internal/apparmor"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +18,21 @@ var Server = echo.New()
|
||||||
var hosts = map[string]*Host{}
|
var hosts = map[string]*Host{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
hatChanged := false
|
||||||
Server.Any("/*", func(c echo.Context) (err error) {
|
Server.Any("/*", func(c echo.Context) (err error) {
|
||||||
|
if !hatChanged {
|
||||||
|
appArmor := config.Config().Listen.AppArmor
|
||||||
|
if appArmor.Serve != "" {
|
||||||
|
if key, err := apparmor.GetMagicToken(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if err := apparmor.ChangeHat(appArmor.Serve, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hatChanged = true
|
||||||
|
}
|
||||||
req := c.Request()
|
req := c.Request()
|
||||||
res := c.Response()
|
res := c.Response()
|
||||||
host := hosts[strings.ToLower(req.Host)]
|
host := hosts[strings.ToLower(req.Host)]
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
<div class="row p-2">
|
<div class="row p-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card border">
|
<div class="card border">
|
||||||
<div class="card-header">Quote</div>
|
<div class="card-header">Welcome</div>
|
||||||
<div class="card-body">
|
<div class="card-body text-center">
|
||||||
<blockquote class="blockquote text-center">
|
<blockquote class="blockquote">
|
||||||
<p>
|
<p>
|
||||||
夜明け前が一番暗い
|
夜明け前が一番暗い
|
||||||
</p>
|
</p>
|
||||||
|
@ -17,6 +17,42 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<hr>
|
||||||
|
<div id="welcome">
|
||||||
|
<p>
|
||||||
|
Welcome to yoake.yumechi.jp, Yumechi's <a target="_blank"
|
||||||
|
href="https://en.wikipedia.org/wiki/Personal_information_manager">
|
||||||
|
<abbr title="Personal Information Manager" class="initialism">PIM</abbr></a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Built with
|
||||||
|
<abbr title="Echo HTTP Framework"><a target="_blank" rel="nofollow noreferer"
|
||||||
|
href="https://echo.labstack.com/">Echo</a></abbr>,
|
||||||
|
<abbr title="Bootstrap CSS Framework"><a target="_blank" rel="nofollow noreferer"
|
||||||
|
href="https://getbootstrap.com/">Bootstrap</a></abbr>,
|
||||||
|
<abbr title="jQuery JavaScript Library"><a href="https://jquery.com/" target="_blank"
|
||||||
|
rel="nofollow noreferer">jQuery</a></abbr>,
|
||||||
|
and
|
||||||
|
<abbr title="Golang Standard Library html/template"><a target="_blank"
|
||||||
|
rel="nofollow noreferer"
|
||||||
|
href="https://pkg.go.dev/html/template">html/template</a></abbr>.
|
||||||
|
Intended to be a parody of the
|
||||||
|
<abbr title="Terumo BCT Trima Accel(R)"><a href="https://www.terumobct.com/trima"
|
||||||
|
target="_blank" rel="nofollow noreferer">Trima Accel (R)</a></abbr> UI.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ if not $auth.Valid }}
|
||||||
|
This website welcomes guests as well!
|
||||||
|
Click around and I hope you cound have some fun!
|
||||||
|
{{ else }}
|
||||||
|
{{ if eq (index $auth.Roles 0) "admin" }}
|
||||||
|
Welcome Home!
|
||||||
|
{{ else }}
|
||||||
|
Hello, my friend! You know the drill!
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,9 +132,61 @@
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-primary" onclick="signin()">Sign In</button>
|
<button class="btn btn-primary" onclick="signin()">Sign In</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<table class="table text-center">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tab Status Icon</th>
|
||||||
|
<th>Meaning</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src={{ trima_img "icon_t_vista_procedure_optimal.gif" "url" }} />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
Tab Selected (Active)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src={{ trima_img "icon_t_vista_procedure_valid.gif" "url" }} />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
Tab Available
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src={{ trima_img "icon_t_vista_procedure_ineligible.gif" "url" }} />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
Addn. Authorization Required
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src={{ trima_img "icon_t_vista_procedure_questionable.gif" "url" }} />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
Unknown
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src={{ trima_img "icon_t_vista_procedure_invalid.gif" "url" }} />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
Access Denied
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
Loading…
Reference in a new issue