mirror of
https://github.com/ordinary-dev/phoenix
synced 2024-09-20 03:40:29 +05:00
Initial commit
This commit is contained in:
commit
16cd7aa635
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
24
assets/auth.css
Normal file
24
assets/auth.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
form {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
48
assets/base.css
Normal file
48
assets/base.css
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(0deg, #1d1926 19px, transparent 1px) center,
|
||||||
|
linear-gradient(90deg, #1d1926 19px, transparent 1px) center,
|
||||||
|
#7a7093;
|
||||||
|
background-size: 20px 20px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 8px 10px;
|
||||||
|
outline: none;
|
||||||
|
background-color: #24272d;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: border-color 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
input:hover,
|
||||||
|
button:active,
|
||||||
|
button:hover {
|
||||||
|
border-color: #812abd;
|
||||||
|
}
|
18
assets/error.css
Normal file
18
assets/error.css
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 1em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
.content {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
32
assets/index.css
Normal file
32
assets/index.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.page {
|
||||||
|
padding: 2em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
.page {
|
||||||
|
padding: 2em 10em
|
||||||
|
}
|
||||||
|
.group {
|
||||||
|
max-width: 230px;
|
||||||
|
}
|
||||||
|
}
|
50
assets/settings.css
Normal file
50
assets/settings.css
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
body {
|
||||||
|
padding: 2em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 1em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.innerForm {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:last-child {
|
||||||
|
padding-bottom: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 3em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-name {
|
||||||
|
width: 130px;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-href {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 2em 10em;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
4
assets/svg/floppy-disk-solid.svg
Normal file
4
assets/svg/floppy-disk-solid.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||||
|
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path fill="white" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V173.3c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32H64zm0 96c0-17.7 14.3-32 32-32H288c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V128zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 561 B |
4
assets/svg/plus-solid.svg
Normal file
4
assets/svg/plus-solid.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||||
|
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path fill="white" d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 446 B |
4
assets/svg/trash-solid.svg
Normal file
4
assets/svg/trash-solid.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||||
|
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path fill="white" d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 519 B |
108
backend/admins.go
Normal file
108
backend/admins.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Admin struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
Username string `gorm:"unique;notNull"`
|
||||||
|
Bcrypt string `gorm:"notNull"`
|
||||||
|
AccessTokens []AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessToken struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
Value string `gorm:"notNull"`
|
||||||
|
AdminID uint64 `gorm:"notNull"`
|
||||||
|
ValidUntil time.Time `gorm:"NotNull"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountAdmins(db *gorm.DB) int64 {
|
||||||
|
var admins []Admin
|
||||||
|
var count int64
|
||||||
|
db.Model(&admins).Count(&count)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAdmin(db *gorm.DB, username string, password string) (Admin, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||||
|
if err != nil {
|
||||||
|
return Admin{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
admin := Admin{
|
||||||
|
Username: username,
|
||||||
|
Bcrypt: string(hash),
|
||||||
|
}
|
||||||
|
result := db.Create(&admin)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return Admin{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthorizeAdmin(db *gorm.DB, username string, password string) (Admin, error) {
|
||||||
|
var admin Admin
|
||||||
|
result := db.Where("username = ?", username).First(&admin)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return Admin{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(admin.Bcrypt), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return Admin{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAccessToken(db *gorm.DB, adminID uint64) (AccessToken, error) {
|
||||||
|
bytes := make([]byte, 64)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return AccessToken{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := AccessToken{
|
||||||
|
AdminID: adminID,
|
||||||
|
Value: base64.StdEncoding.EncodeToString(bytes),
|
||||||
|
// Valid for 1 month
|
||||||
|
ValidUntil: time.Now().AddDate(0, 1, 0),
|
||||||
|
}
|
||||||
|
result := db.Create(&accessToken)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return AccessToken{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save token for 29 days in cookies
|
||||||
|
func SetTokenCookie(c *gin.Context, token AccessToken) {
|
||||||
|
c.SetCookie("phoenix-token", token.Value, 60*60*24*29, "/", "", false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateToken(db *gorm.DB, value string) error {
|
||||||
|
var token AccessToken
|
||||||
|
result := db.Where("value = ?", value).First(&token)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(token.ValidUntil) {
|
||||||
|
return errors.New("Access token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
18
backend/db.go
Normal file
18
backend/db.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDatabaseConnection() (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the schema
|
||||||
|
db.AutoMigrate(&Admin{}, &AccessToken{}, &Group{}, &Link{})
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
32
backend/groups.go
Normal file
32
backend/groups.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"unique,notNull"`
|
||||||
|
Links []Link
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGroups(db *gorm.DB) ([]Group, error) {
|
||||||
|
var groups []Group
|
||||||
|
result := db.Model(&Group{}).Preload("Links").Find(&groups)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateGroup(db *gorm.DB, groupName string) (Group, error) {
|
||||||
|
group := Group{
|
||||||
|
Name: groupName,
|
||||||
|
}
|
||||||
|
result := db.Create(&group)
|
||||||
|
if result.Error != nil {
|
||||||
|
return Group{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
48
backend/links.go
Normal file
48
backend/links.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"notNull"`
|
||||||
|
Href string `gorm:"notNull"`
|
||||||
|
GroupID uint64 `gorm:"notNull"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLink(db *gorm.DB, linkName string, href string, groupID uint64) (Link, error) {
|
||||||
|
link := Link{
|
||||||
|
Name: linkName,
|
||||||
|
Href: href,
|
||||||
|
GroupID: groupID,
|
||||||
|
}
|
||||||
|
result := db.Create(&link)
|
||||||
|
if result.Error != nil {
|
||||||
|
return Link{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLink(db *gorm.DB, id uint64, linkName string, href string) (Link, error) {
|
||||||
|
var link Link
|
||||||
|
db.First(&link, id)
|
||||||
|
|
||||||
|
link.Name = linkName
|
||||||
|
link.Href = href
|
||||||
|
result := db.Save(&link)
|
||||||
|
if result.Error != nil {
|
||||||
|
return Link{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteLink(db *gorm.DB, id uint64) error {
|
||||||
|
result := db.Delete(&Link{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
38
go.mod
Normal file
38
go.mod
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
module github.com/ordinary-dev/phoenix
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.0
|
||||||
|
gorm.io/driver/sqlite v1.4.4
|
||||||
|
gorm.io/gorm v1.24.6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.8.0 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
|
golang.org/x/crypto v0.5.0 // indirect
|
||||||
|
golang.org/x/net v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
|
golang.org/x/text v0.7.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
93
go.sum
Normal file
93
go.sum
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||||
|
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||||
|
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||||
|
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||||
|
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||||
|
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||||
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||||
|
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||||
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
|
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||||
|
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
194
main.go
Normal file
194
main.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/ordinary-dev/phoenix/backend"
|
||||||
|
"github.com/ordinary-dev/phoenix/views"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := backend.GetDatabaseConnection()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
r.LoadHTMLGlob("templates/*")
|
||||||
|
r.Static("/assets", "./assets")
|
||||||
|
|
||||||
|
// Main page
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
groups, err := backend.GetGroups(db)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "index.html.tmpl", gin.H{
|
||||||
|
"groups": groups,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
r.GET("/settings", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
groups, err := backend.GetGroups(db)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "settings.html.tmpl", gin.H{
|
||||||
|
"groups": groups,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
r.POST("/users", func(c *gin.Context) {
|
||||||
|
// If at least 1 administator exists, require authorization
|
||||||
|
if backend.CountAdmins(db) > 0 {
|
||||||
|
tokenValue, err := c.Cookie("phoenix-token")
|
||||||
|
|
||||||
|
// Anonymous visitor
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("At least 1 user exists, you have to sign in first")
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = backend.ValidateToken(db, tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is authorized or no user exists.
|
||||||
|
// Try to create a user.
|
||||||
|
username := c.PostForm("username")
|
||||||
|
password := c.PostForm("password")
|
||||||
|
admin, err := backend.CreateAdmin(db, username, password)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate access token.
|
||||||
|
token, err := backend.CreateAccessToken(db, admin.ID)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backend.SetTokenCookie(c, token)
|
||||||
|
|
||||||
|
// Redirect to homepage.
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create new group
|
||||||
|
r.POST("/groups", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
|
||||||
|
groupName := c.PostForm("groupName")
|
||||||
|
_, err := backend.CreateGroup(db, groupName)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to settings.
|
||||||
|
c.Redirect(http.StatusFound, "/settings")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create new link
|
||||||
|
r.POST("/links", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
|
||||||
|
linkName := c.PostForm("linkName")
|
||||||
|
href := c.PostForm("href")
|
||||||
|
groupID, err := strconv.ParseUint(c.PostForm("groupID"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = backend.CreateLink(db, linkName, href, groupID)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to settings.
|
||||||
|
c.Redirect(http.StatusFound, "/settings")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update link
|
||||||
|
r.POST("/links/:id/put", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
linkName := c.PostForm("linkName")
|
||||||
|
href := c.PostForm("href")
|
||||||
|
|
||||||
|
_, err = backend.UpdateLink(db, id, linkName, href)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to settings.
|
||||||
|
c.Redirect(http.StatusFound, "/settings")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete link
|
||||||
|
r.POST("/links/:id/delete", func(c *gin.Context) {
|
||||||
|
views.RequireAuth(c, db)
|
||||||
|
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = backend.DeleteLink(db, id)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to settings.
|
||||||
|
c.Redirect(http.StatusFound, "/settings")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.POST("/signin", func(c *gin.Context) {
|
||||||
|
// Check credentials.
|
||||||
|
username := c.PostForm("username")
|
||||||
|
password := c.PostForm("password")
|
||||||
|
admin, err := backend.AuthorizeAdmin(db, username, password)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an access token.
|
||||||
|
token, err := backend.CreateAccessToken(db, admin.ID)
|
||||||
|
if err != nil {
|
||||||
|
views.ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backend.SetTokenCookie(c, token)
|
||||||
|
|
||||||
|
// Redirect to homepage.
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Run()
|
||||||
|
}
|
7
readme.md
Normal file
7
readme.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Phoenix
|
||||||
|
|
||||||
|
Self-hosted start page without the extra stuff.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- No javascript
|
||||||
|
- Tiny footprint
|
28
templates/auth.html.tmpl
Normal file
28
templates/auth.html.tmpl
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head"}}
|
||||||
|
<link rel="stylesheet" href="assets/auth.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<form action="{{.formAction}}" method="POST">
|
||||||
|
<h1>{{.title}}</h1>
|
||||||
|
<p>{{.description}}</p>
|
||||||
|
<input
|
||||||
|
placeholder="Username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit">{{.button}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
templates/error.html.tmpl
Normal file
18
templates/error.html.tmpl
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head"}}
|
||||||
|
<link rel="stylesheet" href="assets/error.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="content">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<code>{{.error}}</code>
|
||||||
|
<p>
|
||||||
|
<a href="/">Main page</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
templates/head.html.tmpl
Normal file
6
templates/head.html.tmpl
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>Phoenix</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="assets/base.css" />
|
||||||
|
{{end}}
|
31
templates/index.html.tmpl
Normal file
31
templates/index.html.tmpl
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head"}}
|
||||||
|
<link rel="stylesheet" href="assets/index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<h1>Phoenix</h1>
|
||||||
|
{{if not .groups}}
|
||||||
|
<p>
|
||||||
|
You don't have any links.
|
||||||
|
Go to <a href="/settings">settings</a> and create one.
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{{range .groups}}
|
||||||
|
<div class="group">
|
||||||
|
<h2>{{.Name}}</h2>
|
||||||
|
{{range .Links}}
|
||||||
|
<a href="{{.Href}}" target="_blank" rel="noreferrer">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
93
templates/settings.html.tmpl
Normal file
93
templates/settings.html.tmpl
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head"}}
|
||||||
|
<link rel="stylesheet" href="assets/settings.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<a href="/">Main page</a>
|
||||||
|
|
||||||
|
{{range .groups}}
|
||||||
|
<h2>Group "{{.Name}}"</h2>
|
||||||
|
<form class="row">
|
||||||
|
<input
|
||||||
|
value="{{.Name}}"
|
||||||
|
placeholder="Name"
|
||||||
|
name="groupName"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/assets/svg/floppy-disk-solid.svg" width="16px" height="16px" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{range .Links}}
|
||||||
|
<div class="row">
|
||||||
|
<form method="POST" action="/links/{{.ID}}/put" class="innerForm">
|
||||||
|
<!-- method: put -->
|
||||||
|
<input
|
||||||
|
class="link-name"
|
||||||
|
value="{{.Name}}"
|
||||||
|
name="linkName"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="link-href"
|
||||||
|
value="{{.Href}}"
|
||||||
|
name="href"
|
||||||
|
placeholder="Href"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/assets/svg/floppy-disk-solid.svg" width="16px" height="16px" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/links/{{.ID}}/delete">
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/assets/svg/trash-solid.svg" width="16px" height="16px" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<form action="/links" method="POST" class="row">
|
||||||
|
<input
|
||||||
|
class="link-name"
|
||||||
|
placeholder="Name"
|
||||||
|
name="linkName"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="link-href"
|
||||||
|
placeholder="Href"
|
||||||
|
name="href"
|
||||||
|
type="url"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="groupID"
|
||||||
|
value="{{.ID}}"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/assets/svg/plus-solid.svg" width="16px" height="16px" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<h2>New group</h2>
|
||||||
|
<form method="POST" action="/groups" class="row">
|
||||||
|
<input
|
||||||
|
placeholder="Name"
|
||||||
|
name="groupName"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/assets/svg/plus-solid.svg" width="16px" height="16px" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
52
views/auth.go
Normal file
52
views/auth.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/ordinary-dev/phoenix/backend"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowRegistrationForm(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "auth.html.tmpl", gin.H{
|
||||||
|
"title": "Create an account",
|
||||||
|
"description": "To prevent other people from seeing your links, create an account.",
|
||||||
|
"button": "Create",
|
||||||
|
"formAction": "/users",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowLoginForm(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "auth.html.tmpl", gin.H{
|
||||||
|
"title": "Sign in",
|
||||||
|
"description": "Authorization is required to view this page.",
|
||||||
|
"button": "Sign in",
|
||||||
|
"formAction": "/signin",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires the user to log in before viewing the page.
|
||||||
|
// If successful, does nothing.
|
||||||
|
// In case of an error, it shows the login page or the error page.
|
||||||
|
func RequireAuth(c *gin.Context, db *gorm.DB) {
|
||||||
|
number_of_accounts := backend.CountAdmins(db)
|
||||||
|
|
||||||
|
// First run
|
||||||
|
if number_of_accounts == 0 {
|
||||||
|
ShowRegistrationForm(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenValue, err := c.Cookie("phoenix-token")
|
||||||
|
|
||||||
|
// Anonymous visitor
|
||||||
|
if err != nil {
|
||||||
|
ShowLoginForm(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = backend.ValidateToken(db, tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
ShowError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
16
views/errors.go
Normal file
16
views/errors.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowError(c *gin.Context, err error) {
|
||||||
|
c.HTML(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"error.html.tmpl",
|
||||||
|
gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue