Merge pull request #5 from fossyy/staging

Staging
This commit is contained in:
2024-04-26 15:46:45 +07:00
committed by GitHub
13 changed files with 441 additions and 74 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/filekeeper.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
<methods>
<method importPath="github.com/fossyy/filekeeper/session" receiver="*StoreSession" name="Get" />
</methods>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/filekeeper.iml" filepath="$PROJECT_DIR$/.idea/filekeeper.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -4,23 +4,31 @@ import (
"fmt" "fmt"
"github.com/fossyy/filekeeper/db" "github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/logger" "github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types" "github.com/google/uuid"
"sync" "sync"
"time" "time"
) )
type Cache struct { type Cache struct {
users map[string]*types.UserWithExpired users map[string]*UserWithExpired
mu sync.Mutex mu sync.Mutex
} }
type UserWithExpired struct {
UserID uuid.UUID
Username string
Email string
Password string
AccessAt time.Time
}
var log *logger.AggregatedLogger var log *logger.AggregatedLogger
var UserCache *Cache var UserCache *Cache
func init() { func init() {
log = logger.Logger() log = logger.Logger()
UserCache = &Cache{users: make(map[string]*types.UserWithExpired)} UserCache = &Cache{users: make(map[string]*UserWithExpired)}
ticker := time.NewTicker(time.Hour * 8) ticker := time.NewTicker(time.Hour * 8)
go func() { go func() {
@ -44,7 +52,7 @@ func init() {
}() }()
} }
func Get(email string) (*types.UserWithExpired, error) { func Get(email string) (*UserWithExpired, error) {
UserCache.mu.Lock() UserCache.mu.Lock()
defer UserCache.mu.Unlock() defer UserCache.mu.Unlock()
@ -52,13 +60,13 @@ func Get(email string) (*types.UserWithExpired, error) {
return user, nil return user, nil
} }
var userData types.UserWithExpired var userData UserWithExpired
err := db.DB.Table("users").Where("email = ?", email).First(&userData).Error err := db.DB.Table("users").Where("email = ?", email).First(&userData).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
UserCache.users[email] = &types.UserWithExpired{ UserCache.users[email] = &UserWithExpired{
UserID: userData.UserID, UserID: userData.UserID,
Username: userData.Username, Username: userData.Username,
Email: userData.Email, Email: userData.Email,

View File

@ -9,6 +9,7 @@ import (
"github.com/fossyy/filekeeper/utils" "github.com/fossyy/filekeeper/utils"
signinView "github.com/fossyy/filekeeper/view/signin" signinView "github.com/fossyy/filekeeper/view/signin"
"net/http" "net/http"
"strings"
) )
var log *logger.AggregatedLogger var log *logger.AggregatedLogger
@ -64,8 +65,21 @@ func POST(w http.ResponseWriter, r *http.Request) {
Authenticated: true, Authenticated: true,
} }
userAgent := r.Header.Get("User-Agent")
browserInfo, osInfo := parseUserAgent(userAgent)
sessionInfo := session.SessionInfo{
SessionID: storeSession.ID,
Browser: browserInfo["browser"],
Version: browserInfo["version"],
OS: osInfo["os"],
OSVersion: osInfo["version"],
IP: utils.ClientIP(r),
Location: "Indonesia",
}
storeSession.Save(w) storeSession.Save(w)
session.AppendSession(email, storeSession) session.AppendSession(email, &sessionInfo)
cookie, err := r.Cookie("redirect") cookie, err := r.Cookie("redirect")
if errors.Is(err, http.ErrNoCookie) { if errors.Is(err, http.ErrNoCookie) {
@ -90,3 +104,64 @@ func POST(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
func parseUserAgent(userAgent string) (map[string]string, map[string]string) {
browserInfo := make(map[string]string)
osInfo := make(map[string]string)
if strings.Contains(userAgent, "Firefox") {
browserInfo["browser"] = "Firefox"
parts := strings.Split(userAgent, "Firefox/")
if len(parts) > 1 {
version := strings.Split(parts[1], " ")[0]
browserInfo["version"] = version
}
} else if strings.Contains(userAgent, "Chrome") {
browserInfo["browser"] = "Chrome"
parts := strings.Split(userAgent, "Chrome/")
if len(parts) > 1 {
version := strings.Split(parts[1], " ")[0]
browserInfo["version"] = version
}
} else {
browserInfo["browser"] = "Unknown"
browserInfo["version"] = "Unknown"
}
if strings.Contains(userAgent, "Windows") {
osInfo["os"] = "Windows"
parts := strings.Split(userAgent, "Windows ")
if len(parts) > 1 {
version := strings.Split(parts[1], ";")[0]
osInfo["version"] = version
}
} else if strings.Contains(userAgent, "Macintosh") {
osInfo["os"] = "Mac OS"
parts := strings.Split(userAgent, "Mac OS X ")
if len(parts) > 1 {
version := strings.Split(parts[1], ";")[0]
osInfo["version"] = version
}
} else if strings.Contains(userAgent, "Linux") {
osInfo["os"] = "Linux"
osInfo["version"] = "Unknown"
} else if strings.Contains(userAgent, "Android") {
osInfo["os"] = "Android"
parts := strings.Split(userAgent, "Android ")
if len(parts) > 1 {
version := strings.Split(parts[1], ";")[0]
osInfo["version"] = version
}
} else if strings.Contains(userAgent, "iPhone") || strings.Contains(userAgent, "iPad") || strings.Contains(userAgent, "iPod") {
osInfo["os"] = "iOS"
parts := strings.Split(userAgent, "OS ")
if len(parts) > 1 {
version := strings.Split(parts[1], " ")[0]
osInfo["version"] = version
}
} else {
osInfo["os"] = "Unknown"
osInfo["version"] = "Unknown"
}
return browserInfo, osInfo
}

View File

@ -34,7 +34,8 @@ func GET(w http.ResponseWriter, r *http.Request) {
log.Error(err.Error()) log.Error(err.Error())
return return
} }
component := userView.Main("User Page", userSession.Email, userSession.Username)
component := userView.Main("User Page", userSession.Email, userSession.Username, session.UserSessions[userSession.Email])
err = component.Render(r.Context(), w) err = component.Render(r.Context(), w)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -75,6 +75,7 @@ func Auth(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
return return
} }
storeSession, err := session.Store.Get(cookie.Value) storeSession, err := session.Store.Get(cookie.Value)
if err != nil { if err != nil {
if errors.Is(err, &session.SessionNotFound{}) { if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w) storeSession.Destroy(w)
@ -87,6 +88,7 @@ func Auth(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
} }
userSession := GetUser(storeSession) userSession := GetUser(storeSession)
if userSession.Authenticated { if userSession.Authenticated {
session.GetSessionInfo(storeSession.Values["user"].(types.User).Email, cookie.Value).UpdateAccessTime()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }

View File

@ -1,6 +1,7 @@
package routes package routes
import ( import (
"encoding/json"
downloadHandler "github.com/fossyy/filekeeper/handler/download" downloadHandler "github.com/fossyy/filekeeper/handler/download"
downloadFileHandler "github.com/fossyy/filekeeper/handler/download/file" downloadFileHandler "github.com/fossyy/filekeeper/handler/download/file"
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword" forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
@ -15,6 +16,7 @@ import (
"github.com/fossyy/filekeeper/handler/upload/initialisation" "github.com/fossyy/filekeeper/handler/upload/initialisation"
userHandler "github.com/fossyy/filekeeper/handler/user" userHandler "github.com/fossyy/filekeeper/handler/user"
"github.com/fossyy/filekeeper/middleware" "github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/session"
"net/http" "net/http"
) )
@ -35,6 +37,10 @@ func SetupRoutes() *http.ServeMux {
} }
}) })
handler.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(session.Getses())
})
handler.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) { handler.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
"time"
) )
type Session struct { type Session struct {
@ -17,8 +18,21 @@ type StoreSession struct {
mu sync.Mutex mu sync.Mutex
} }
type SessionInfo struct {
SessionID string
Browser string
Version string
OS string
OSVersion string
IP string
Location string
AccessAt string
}
type ListSessionInfo map[string][]*SessionInfo
var Store = StoreSession{Sessions: make(map[string]*Session)} var Store = StoreSession{Sessions: make(map[string]*Session)}
var userSessions = make(map[string][]string) var UserSessions = make(ListSessionInfo)
type SessionNotFound struct{} type SessionNotFound struct{}
@ -68,33 +82,48 @@ func (s *Session) Destroy(w http.ResponseWriter) {
}) })
} }
func AppendSession(email string, session *Session) { func AppendSession(email string, sessionInfo *SessionInfo) {
userSessions[email] = append(userSessions[email], session.ID) UserSessions[email] = append(UserSessions[email], sessionInfo)
} }
func RemoveSession(email string, id string) { func RemoveSession(email string, id string) {
sessions := userSessions[email] sessions := UserSessions[email]
var updatedSessions []string var updatedSessions []*SessionInfo
for _, userSession := range sessions { for _, userSession := range sessions {
if userSession != id { if userSession.SessionID != id {
updatedSessions = append(updatedSessions, userSession) updatedSessions = append(updatedSessions, userSession)
} }
} }
if len(updatedSessions) > 0 { if len(updatedSessions) > 0 {
userSessions[email] = updatedSessions UserSessions[email] = updatedSessions
return return
} }
delete(userSessions, email) delete(UserSessions, email)
} }
func RemoveAllSession(email string) { func RemoveAllSession(email string) {
sessions := userSessions[email] sessions := UserSessions[email]
for _, session := range sessions { for _, session := range sessions {
delete(Store.Sessions, session) delete(Store.Sessions, session.SessionID)
} }
delete(userSessions, email) delete(UserSessions, email)
} }
func Getses() map[string][]string { func GetSessionInfo(email string, id string) *SessionInfo {
return userSessions for _, session := range UserSessions[email] {
if session.SessionID == id {
return session
}
}
return nil
}
func (user *SessionInfo) UpdateAccessTime() {
currentTime := time.Now()
formattedTime := currentTime.Format("01-02-2006")
user.AccessAt = formattedTime
}
func Getses() *ListSessionInfo {
return &UserSessions
} }

View File

@ -2,7 +2,6 @@ package types
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"time"
) )
type Message struct { type Message struct {
@ -17,14 +16,6 @@ type User struct {
Authenticated bool Authenticated bool
} }
type UserWithExpired struct {
UserID uuid.UUID
Username string
Email string
Password string
AccessAt time.Time
}
type FileInfo struct { type FileInfo struct {
Name string `json:"name"` Name string `json:"name"`
Size int `json:"size"` Size int `json:"size"`

View File

@ -1,53 +1,267 @@
package userView package userView
import "github.com/fossyy/filekeeper/view/layout" import (
"github.com/fossyy/filekeeper/view/layout"
"github.com/fossyy/filekeeper/session"
)
templ content(email string, username string, title string) { templ content(email string, username string, title string, ListSession []*session.SessionInfo) {
@layout.Base(title){ @layout.Base(title){
<div class="bg-white overflow-hidden shadow rounded-lg border"> <main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
<div class="px-4 py-5 sm:px-6"> <div class="grid gap-10 lg:grid-cols-[1fr_300px]">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <div class="space-y-8">
User Profile <section>
</h3> <h2 class="text-2xl font-bold tracking-tight">Profile</h2>
<p class="mt-1 max-w-2xl text-sm text-gray-500"> <div class="mt-6 grid gap-6">
This is some information about the user. <div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="name">
Name
</label>
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
id="name" placeholder="Enter your name" value={username} disabled />
</div>
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="email">
Email
</label>
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
type="email" id="email" placeholder="Enter your email" value={email} disabled />
</div>
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="profile-picture">
Profile Picture
</label>
<div class="flex items-center gap-4">
<div
class="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-gray-100 rounded-full">
<span class="font-medium text-gray-600">JL</span>
</div>
<button
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="mr-2 h-4 w-4">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" x2="12" y1="3" y2="15"></line>
</svg>
Upload
</button>
</div>
</div>
</div>
</section>
<section>
<h2 class="text-2xl font-bold tracking-tight">Session Management</h2>
<div class="mt-6 grid gap-6">
<div class="grid gap-2">
<div class="flex items-center justify-between">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="two-factor">
Two-Factor Authentication
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked />
<div
class="relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600">
</div>
</label>
<input type="checkbox" aria-hidden="true"
style="transform:translateX(-100%);position:absolute;pointer-events:none;opacity:0;margin:0"
tabindex="-1" value="on" />
</div>
</div>
<div class="grid gap-2">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="relative w-full overflow-auto">
<table class="w-full caption-bottom text-sm">
<thead class="[&amp;_tr]:border-b">
<tr
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
<th
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0">
IP Address
</th>
<th
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0">
Browser
</th>
<th
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0">
Device
</th>
<th
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0">
Last Activity
</th>
<th
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0">
Actions
</th>
</tr>
</thead>
<tbody class="[&amp;_tr:last-child]:border-0">
for _, ses := range ListSession {
<tr
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.IP}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.Browser + ses.Version}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.OS + ses.OSVersion}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.AccessAt}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">
<a
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
type="button" id="radix-:rq:" aria-haspopup="menu"
aria-expanded="false" data-state="closed"
href="/">
Terminate
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
<section>
<h2 class="text-2xl font-bold tracking-tight">Reset Password</h2>
<div class="mt-6 grid gap-6">
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="name">
Current password
</label>
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
type="password" id="name" placeholder="Current password" />
</div>
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="name">
New password
</label>
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
type="password" id="name" placeholder="New password" />
</div>
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="name">
New password confirmation
</label>
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
type="password" id="name" placeholder="New password confirmation" />
</div>
<button type="button" class="focus:outline-none text-white bg-red-500 hover:bg-red-700 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2">Update password</button>
</div>
</section>
<div class="grid gap-1">
<div class="flex items-center justify-between">
<button
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="mr-2 h-4 w-4">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" x2="9" y1="12" y2="12"></line>
</svg>
Log Out
</button>
</div>
<p class="text-sm text-gray-500">
Click to log out or terminate the current session.
</p> </p>
</div> </div>
<div class="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl class="sm:divide-y sm:divide-gray-200">
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Full name
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{ username }
</dd>
</div> </div>
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500"> <div class="space-y-8">
Email address <div class="rounded-lg border bg-card text-card-foreground shadow-sm" data-v0-t="card">
</dt> <div class="flex flex-col space-y-1.5 p-6">
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <h3 class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight">Storage Usage
{ email } </h3>
</dd>
</div> </div>
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="p-6 grid gap-4">
<dt class="text-sm font-medium text-gray-500"> <div class="flex items-center justify-between">
Password <span>Used</span>
</dt> <span>42.0GB</span>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
ntah lah
</dd>
</div> </div>
</dl> <div class="w-full bg-gray-300 rounded-full h-2.5">
<div class="bg-gray-800 h-2.5 rounded-full" style="width: 45%"></div>
</div> </div>
<form action="/logout" method="get"> <div class="flex items-center justify-between">
<button type="submit" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Logout</button> <span>Available</span>
</form> <span>6.9GB</span>
</div> </div>
<div class="w-full bg-gray-300 rounded-full h-2.5">
<div class="bg-gray-800 h-2.5 rounded-full" style="width: 100%"></div>
</div>
<a
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
type="button" id="radix-:rq:" aria-haspopup="menu"
aria-expanded="false" data-state="closed"
href="/upload">
Upload
</a>
<a
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
type="button" id="radix-:rq:" aria-haspopup="menu"
aria-expanded="false" data-state="closed"
href="/download">
Download
</a>
</div>
</div>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm" data-v0-t="card">
<div class="flex flex-col space-y-1.5 p-6">
<h3 class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight">Upgrade Storage
</h3>
</div>
<div class="p-6 grid gap-4">
<div class="grid gap-2">
<h3 class="text-lg font-semibold">Pro Plan</h3>
<p class="text-gray-500">50GB of storage for $9.99/month</p>
<button
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2">
Upgrade
</button>
</div>
<div class="grid gap-2">
<h3 class="text-lg font-semibold">Enterprise Plan</h3>
<p class="text-gray-500">1TB of storage for $49.99/month</p>
<button
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2">
Upgrade
</button>
</div>
</div>
</div>
</div>
</div>
</main>
} }
} }
templ Main(title string, email string, username string) { templ Main(title string, email string, username string, ListSession []*session.SessionInfo) {
@content(email, username, title) @content(email, username, title, ListSession)
} }