User center page

This commit is contained in:
LittleSheep 2024-04-21 12:20:06 +08:00
parent ee6e7324b2
commit 6b26cad796
23 changed files with 353 additions and 61 deletions

2
.idea/Passport.iml generated
View File

@ -6,5 +6,7 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="animate.css" level="application" />
<orderEntry type="library" name="tailwindcss" level="application" />
<orderEntry type="library" name="@tailwindcss/typography" level="application" />
</component>
</module>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{animate.css}" />
<file url="PROJECT" libraries="{@tailwindcss/typography, animate.css, tailwindcss}" />
</component>
</project>

40
.idea/workspace.xml generated
View File

@ -4,28 +4,30 @@
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Sign up &amp; Sign in">
<change afterPath="$PROJECT_DIR$/pkg/server/ui/accounts.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/server/ui/mfa.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/services/mfa.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/views/mfa-apply.gohtml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/views/mfa.gohtml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/views/users/me/index.gohtml" afterDir="false" />
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: An entire complete sign in user flow">
<change afterPath="$PROJECT_DIR$/pkg/views/layouts/user-center.gohtml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pkg/views/users/me.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/Passport.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/Passport.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/jsLibraryMappings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/jsLibraryMappings.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.en.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/models/accounts.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/models/accounts.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/models/auth.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/models/auth.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/models/clients.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/models/clients.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/accounts_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/accounts_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/oauth_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/oauth_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/security_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/security_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/startup.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/startup.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/ui/accounts.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/accounts.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/ui/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/ui/signin.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/signin.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/ui/signup.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/signup.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/factors.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/factors.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/server/userinfo_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/userinfo_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/auth.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/auth.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/ticker_maintainer.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticker_maintainer.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/ticket.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/ticket_token.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket_token.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/utils/request.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/utils/request.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/views/signin.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signin.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/views/signup.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signup.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/settings.toml" beforeDir="false" afterPath="$PROJECT_DIR$/settings.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/views/partials/header.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/partials/header.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/views/users/me/index.gohtml" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -93,6 +95,7 @@
<recent name="$PROJECT_DIR$/pkg" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/pkg/views/users" />
<recent name="$PROJECT_DIR$/pkg/utils" />
<recent name="$PROJECT_DIR$/pkg/services" />
</key>
@ -135,7 +138,8 @@
<MESSAGE value=":recycle: Refactor frontend" />
<MESSAGE value=":sparkles: New ticket ways" />
<MESSAGE value=":sparkles: Sign up &amp; Sign in" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Sign up &amp; Sign in" />
<MESSAGE value=":sparkles: An entire complete sign in user flow" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: An entire complete sign in user flow" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>

1
go.mod
View File

@ -45,6 +45,7 @@ require (
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect

2
go.sum
View File

@ -86,6 +86,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM=
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=

View File

@ -23,8 +23,8 @@ type Account struct {
PersonalPage AccountPage `json:"personal_page"`
Contacts []AccountContact `json:"contacts"`
Sessions []AuthTicket `json:"sessions"`
Factors []AuthFactor `json:"factors"`
Tickets []AuthTicket `json:"tickets"`
Factors []AuthFactor `json:"factors"`
Events []ActionEvent `json:"events"`
MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"`

View File

@ -45,20 +45,20 @@ type AuthTicket struct {
func (v AuthTicket) IsAvailable() error {
if v.RequireMFA || v.RequireAuthenticate {
return fmt.Errorf("session isn't authenticated yet")
return fmt.Errorf("ticket isn't authenticated yet")
}
if v.AvailableAt != nil && time.Now().Unix() < v.AvailableAt.Unix() {
return fmt.Errorf("session isn't available yet")
return fmt.Errorf("ticket isn't available yet")
}
if v.ExpiredAt != nil && time.Now().Unix() > v.ExpiredAt.Unix() {
return fmt.Errorf("session expired")
return fmt.Errorf("ticket expired")
}
return nil
}
type AuthContext struct {
Ticket AuthTicket `json:"session"`
Ticket AuthTicket `json:"ticket"`
Account Account `json:"account"`
ExpiredAt time.Time `json:"expired_at"`
}

View File

@ -11,7 +11,7 @@ type ThirdClient struct {
Secret string `json:"secret"`
Urls datatypes.JSONSlice[string] `json:"urls"`
Callbacks datatypes.JSONSlice[string] `json:"callbacks"`
Sessions []AuthTicket `json:"sessions" gorm:"foreignKey:ClientID"`
Sessions []AuthTicket `json:"tickets" gorm:"foreignKey:ClientID"`
Notifications []Notification `json:"notifications" gorm:"foreignKey:SenderID"`
IsDraft bool `json:"is_draft"`
AccountID *uint `json:"account_id"`

View File

@ -113,7 +113,7 @@ func editUserinfo(c *fiber.Ctx) error {
func killSession(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("sessionId", 0)
id, _ := c.ParamsInt("ticketId", 0)
if err := database.C.Delete(&models.AuthTicket{}, &models.AuthTicket{
BaseModel: models.BaseModel{ID: uint(id)},

View File

@ -28,29 +28,29 @@ func preConnect(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var session models.AuthTicket
var ticket models.AuthTicket
if err := database.C.Where(&models.AuthTicket{
AccountID: user.ID,
ClientID: &client.ID,
}).Where("last_grant_at IS NULL").First(&session).Error; err == nil {
if session.ExpiredAt != nil && session.ExpiredAt.Unix() < time.Now().Unix() {
}).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil {
if ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix() {
return c.JSON(fiber.Map{
"client": client,
"session": nil,
"client": client,
"ticket": nil,
})
} else {
session, err = services.RegenSession(session)
ticket, err = services.RegenSession(ticket)
}
return c.JSON(fiber.Map{
"client": client,
"session": session,
"client": client,
"ticket": ticket,
})
}
return c.JSON(fiber.Map{
"client": client,
"session": nil,
"client": client,
"ticket": nil,
})
}
@ -86,7 +86,7 @@ func doConnect(c *fiber.Ctx) error {
} else {
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
return c.JSON(fiber.Map{
"session": ticket,
"ticket": ticket,
"redirect_uri": redirect,
})
}

View File

@ -12,7 +12,7 @@ func getTickets(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0)
var count int64
var sessions []models.AuthTicket
var tickets []models.AuthTicket
if err := database.C.
Where(&models.AuthTicket{AccountID: user.ID}).
Model(&models.AuthTicket{}).
@ -25,12 +25,12 @@ func getTickets(c *fiber.Ctx) error {
Where(&models.AuthTicket{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&sessions).Error; err != nil {
Find(&tickets).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": sessions,
"data": tickets,
})
}

View File

@ -88,7 +88,7 @@ func NewServer() {
me.Put("/page", authMiddleware, editPersonalPage)
me.Get("/events", authMiddleware, getEvents)
me.Get("/tickets", authMiddleware, getTickets)
me.Delete("/sessions/:sessionId", authMiddleware, killSession)
me.Delete("/tickets/:ticketId", authMiddleware, killSession)
me.Post("/confirm", doRegisterConfirm)

View File

@ -1,7 +1,48 @@
package ui
import "github.com/gofiber/fiber/v2"
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"github.com/gofiber/fiber/v2"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/sujit-baniya/flash"
"html/template"
"time"
)
func selfUserinfoPage(c *fiber.Ctx) error {
return c.Render("views/users/me/index", fiber.Map{})
user := c.Locals("principal").(models.Account)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
Preload("PersonalPage").
Preload("Contacts").
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var birthday = "Unknown"
if data.Profile.Birthday != nil {
birthday = data.Profile.Birthday.Format(time.RFC822)
}
doc := parser.
NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock).
Parse([]byte(data.PersonalPage.Content))
renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank})
return c.Render("views/users/me", fiber.Map{
"info": flash.Get(c)["message"],
"uid": fmt.Sprintf("%08d", data.ID),
"joined_at": data.CreatedAt.Format(time.RFC822),
"birthday_at": birthday,
"personal_page": template.HTML(markdown.Render(doc, renderer)),
"userinfo": data,
}, "views/layouts/user-center")
}

View File

@ -12,12 +12,10 @@ func MapUserInterface(A *fiber.App, authFunc func(c *fiber.Ctx, overrides ...str
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
token = cookie
}
fmt.Println(token)
c.Locals("token", token)
if err := authFunc(c); err != nil {
fmt.Println(err)
uri := c.Request().URI().FullURI()
return c.Redirect(fmt.Sprintf("/sign-in?redirect_uri=%s", string(uri)))
} else {

View File

@ -12,7 +12,7 @@ func getOtherUserinfo(c *fiber.Ctx) error {
var account models.Account
if err := database.C.
Where(&models.Account{Name: alias}).
Omit("sessions", "challenges", "factors", "events", "clients", "notifications", "notify_subscribers").
Omit("tickets", "challenges", "factors", "events", "clients", "notifications", "notify_subscribers").
Preload("Profile").
First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())

View File

@ -85,14 +85,14 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
var ctx models.AuthContext
// Query data from primary database
session, err := GetTicketWithToken(jti)
ticket, err := GetTicketWithToken(jti)
if err != nil {
return ctx, fmt.Errorf("invalid auth session: %v", err)
} else if err := session.IsAvailable(); err != nil {
return ctx, fmt.Errorf("unavailable auth session: %v", err)
return ctx, fmt.Errorf("invalid auth ticket: %v", err)
} else if err := ticket.IsAvailable(); err != nil {
return ctx, fmt.Errorf("unavailable auth ticket: %v", err)
}
user, err := GetAccount(session.AccountID)
user, err := GetAccount(ticket.AccountID)
if err != nil {
return ctx, fmt.Errorf("invalid account: %v", err)
}
@ -100,7 +100,7 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
// Every context should expires in some while
// Once user update their account info, this will have delay to update
ctx = models.AuthContext{
Ticket: session,
Ticket: ticket,
Account: user,
ExpiredAt: time.Now().Add(5 * time.Minute),
}

View File

@ -14,7 +14,7 @@ func DoAutoSignoff() {
duration := time.Duration(viper.GetInt64("security.auto_signoff_duration")) * time.Second
divider := time.Now().Add(-duration)
log.Debug().Time("before", divider).Msg("Now signing off sessions...")
log.Debug().Time("before", divider).Msg("Now signing off tickets...")
if tx := database.C.
Where("last_grant_at < ?", divider).

View File

@ -45,7 +45,7 @@ func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
UserAgent: ua,
RequireMFA: requireMFA,
RequireAuthenticate: true,
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
ExpiredAt: nil,
AvailableAt: nil,
AccountID: user.ID,
}

View File

@ -7,7 +7,7 @@
<div class="wrapper-container">
<div class="wrapper-middleware">
{{if ne .info nil}}
<div class="animate__animated animate__fadeInDown alert">
<div class="alert">
<div class="content">{{.info}}</div>
</div>
{{end}}
@ -26,6 +26,8 @@
display: flex;
justify-content: center;
align-items: center;
background-color: var(--md-sys-color-surface-container);
}
.wrapper-middleware {

View File

@ -0,0 +1,87 @@
<!doctype html>
<html lang="en">
{{template "views/partials/header"}}
<body>
<div class="wrapper-container">
<div class="wrapper-middleware">
{{if ne .info nil}}
<div class="alert">
<div class="content">{{.info}}</div>
</div>
{{end}}
<div class="wrapper-card">
{{embed}}
</div>
</div>
</div>
</body>
<style>
.wrapper-container {
width: 100dvw;
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
scrollbar-width: none;
background-color: var(--md-sys-color-surface-container);
}
.wrapper-container::-webkit-scrollbar, body::-webkit-scrollbar {
display: none;
width: 0;
}
.wrapper-middleware {
width: 100%;
min-width: 0;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.wrapper-card {
transition: all .3s;
height: auto;
overflow: auto;
display: grid;
grid-template-columns: 1fr;
justify-content: center;
border-radius: 28px;
padding: 56px;
gap: 2rem;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface)
}
.alert {
padding: 16px;
border-radius: 16px;
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
display: flex;
gap: 8px;
}
.alert .content {
flex-grow: 1;
text-transform: capitalize;
}
@media (min-width: 768px) {
.wrapper-card {
grid-template-columns: 1fr 1fr;
}
}
</style>
</html>

View File

@ -9,10 +9,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<script type="importmap">
@ -73,6 +72,14 @@
--md-sys-color-on-error-container: #410002;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
html, body {
padding: 0;
margin: 0;

151
pkg/views/users/me.gohtml Normal file
View File

@ -0,0 +1,151 @@
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/base.min.css">
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/components.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tailwindcss/typography@0.1.2/dist/typography.min.css">
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/utilities.min.css">
<div class="left-part name-card">
{{$bannerLength := len .userinfo.Banner}} {{if gt $bannerLength 0}}
{{end}}
{{$avatarLength := len .userinfo.Avatar}} {{if gt $avatarLength 0}}
<img src="/api/avatar/{{.userinfo.Avatar}}" alt="Avatar" class="avatar">
{{else}}
<div class="avatar empty">
<span class="material-symbols-outlined">account_circle</span>
</div>
{{end}}
<div class="name">
<h2 class="username">{{.userinfo.Nick}}</h2>
<h6 class="nickname">@{{.userinfo.Name}}</h6>
</div>
{{$descriptionLength := len .userinfo.Description}} {{if gt $descriptionLength 0}}
<div class="description">{{.userinfo.Description}}</div>
{{else}}
<div class="description empty">No description yet.</div>
{{end}}
<div class="uid">#{{.uid}}</div>
<div class="metadata">
<div><span class="material-symbols-outlined">calendar_month</span> {{.joined_at}}</div>
<div><span class="material-symbols-outlined">cake</span> {{.birthday_at}}</div>
</div>
<div class="actions">
<md-filled-tonal-button class="action" href="/users/me/personalize">
Personalize
<span slot="icon" class="material-symbols-outlined">palette</span>
</md-filled-tonal-button>
<md-filled-tonal-button class="action" href="/users/me/security">
Security
<span slot="icon" class="material-symbols-outlined">security</span>
</md-filled-tonal-button>
</div>
</div>
<div class="right-part">
<article class="personal-page prose">
{{.personal_page}}
</article>
</div>
<style>
.avatar {
display: block;
width: 64px;
height: 64px;
clip-path: circle();
}
.avatar.empty {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
.name-card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.name-card .name {
display: flex;
align-items: baseline;
gap: 0.3rem;
}
.name-card .username {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.name-card .nickname {
margin: 0;
font-size: 0.75rem;
font-weight: 500;
}
.name-card .uid {
margin-top: -0.8rem;
font-size: 0.7rem;
font-weight: 400;
font-family: Roboto Mono, monospace;
}
.name-card .description {
margin-top: -1.25rem;
}
.description.empty {
font-style: italic;
}
.name-card .metadata {
font-size: 0.85rem;
font-weight: 500;
display: flex;
flex-direction: column;
}
.metadata > div {
display: flex;
align-items: center;
gap: 0.25rem;
}
.metadata .material-symbols-outlined {
font-size: 1rem;
display: block;
}
.actions {
display: flex;
gap: 0.5rem;
margin: 0 -0.5rem;
}
@media (min-width: 768px) {
.actions {
flex-direction: column;
}
}
.actions .action {
width: fit-content;
}
.actions .material-symbols-outlined {
font-size: 20px;
margin-bottom: 4px;
}
.left-part .prose {
min-width: 0;
max-width: unset;
}
</style>

View File

@ -1,3 +0,0 @@
<h1>
You are accepted!
</h1>