打入补丁:9F1436E39C95D59E.patch

pull/7939/head
宋子桓🌈 2025-03-20 23:23:29 +08:00
parent 040a86d8c0
commit 5dc50b297f
No known key found for this signature in database
GPG Key ID: BEF31CC2410A2E8A
24 changed files with 230 additions and 202 deletions

1
go.mod
View File

@ -73,6 +73,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisenkom/go-mssqldb v0.12.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/djherbis/buffer v1.2.0 // indirect
github.com/djherbis/nio/v3 v3.0.1 // indirect

2
go.sum
View File

@ -58,6 +58,8 @@ github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58s
github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
github.com/derision-test/go-mockgen v1.3.7 h1:b/DXAXL2FkaRPpnbYK3ODdZzklmJAwox0tkc6yyXx74=
github.com/derision-test/go-mockgen v1.3.7/go.mod h1:/TXUePlhtHmDDCaDAi/a4g6xOHqMDz3Wf0r2NPGskB4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o=

View File

@ -15,7 +15,6 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/email"
"gogs.io/gogs/internal/markup"
"gogs.io/gogs/internal/userutil"
)
func (issue *Issue) MailSubject() string {
@ -43,16 +42,6 @@ func (this mailerUser) PublicEmail() string {
return this.user.PublicEmail
}
func (this mailerUser) GenerateEmailActivateCode(email string) string {
return userutil.GenerateActivateCode(
this.user.ID,
email,
this.user.Name,
this.user.Password,
this.user.Rands,
)
}
func NewMailerUser(u *User) email.User {
return mailerUser{u}
}

View File

@ -1,6 +1,6 @@
// Copyright 2022 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations

View File

@ -1,6 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations
@ -52,22 +52,22 @@ var migrations = []Migration{
// v18 -> v19:v0.11.55
// NewMigration("clean unlinked webhook and hook_tasks", cleanUnlinkedWebhookAndHookTasks),
// v19 -> v20:v0.13.0
// v19 -> v20:v0.13.0Gogs
NewMigration("migrate access tokens to store SHA56", migrateAccessTokenToSHA256),
// v20 -> v21:v0.13.0
// v20 -> v21:v0.13.0Gogs
NewMigration("add index to action.user_id", addIndexToActionUserID),
// v21 -> v22:v0.13.0
// v21 -> v22:v0.13.0Gogs
//
// NOTE: There was a bug in calculating the value of the `version.version`
// column after a migration is done, thus some instances are on v21 but some are
// on v22. Let's make a noop v22 to make sure every instance will not miss a
// real future migration.
NewMigration("noop", func(*gorm.DB) error { return nil }),
// v22 -> v23:v0.14.0
// v22 -> v23:v0.14.0Gogs
NewMigration("add user.public_email column", addUserPublicEmail),
// v23 -> v24:v0.14.0
// v23 -> v24:v0.14.0Gogs
NewMigration("add user.local_email column", addUserLocalEmail),
// v24 -> v25:v0.14.0
// v24 -> v25:v0.14.0Gogs v24:v1.0.0Gogs
NewMigration("insert user primary to database", insertUserPrimaryEmail),
}

View File

@ -1,6 +1,6 @@
// Copyright 2022 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations

View File

@ -1,6 +1,6 @@
// Copyright 2022 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations

View File

@ -1,6 +1,6 @@
// Copyright 2022 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations

View File

@ -1,6 +1,6 @@
// Copyright 2022 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package migrations

View File

@ -1,11 +1,16 @@
package migrations
import (
"fmt"
"gorm.io/gorm"
)
func addUserPublicEmail(db *gorm.DB) error {
type User struct {
PublicEmail string // 不能使用NOT NULL
}
type UserNotNull struct {
PublicEmail string `xorm:"NOT NULL" gorm:"not null"`
}
@ -16,12 +21,17 @@ func addUserPublicEmail(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
err := tx.Migrator().AddColumn(&User{}, "PublicEmail")
if err != nil {
return err
return fmt.Errorf("add column user.public_email error: %s", err.Error())
}
err = tx.Exec("UPDATE `user` SET `public_email` = `email` WHERE `public_email` = '' AND `type` = 0").Error
if err != nil {
return err
return fmt.Errorf("update public_email error: %s", err.Error())
}
err = tx.Debug().Migrator().AlterColumn(&UserNotNull{}, "PublicEmail")
if err != nil {
return fmt.Errorf("alter column user.public_email error: %s", err.Error())
}
return nil

View File

@ -1,15 +1,20 @@
package migrations
import (
"github.com/pkg/errors"
"fmt"
gouuid "github.com/satori/go.uuid"
"gorm.io/gorm"
)
func addUserLocalEmail(db *gorm.DB) error {
type User struct {
ID int64 `gorm:"primaryKey"`
LocalEmail string `xorm:"NOT NULL" gorm:"not null"`
ID int64 `gorm:"primaryKey"`
LocalEmail string
}
type UserNotNULL struct {
ID int64 `gorm:"primaryKey"`
LocalEmail string
}
if db.Migrator().HasColumn(&User{}, "LocalEmail") {
@ -19,7 +24,7 @@ func addUserLocalEmail(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
err := tx.Migrator().AddColumn(&User{}, "LocalEmail")
if err != nil {
return err
return fmt.Errorf("add column user.local_email error: %s", err.Error())
}
const limit = 100
@ -27,14 +32,14 @@ func addUserLocalEmail(db *gorm.DB) error {
var res []User
err := tx.Table("user").Where("type = ?", 0).Where("local_email = ''").Limit(limit).Find(&res).Error
if err != nil {
return errors.Wrap(err, "query user")
return fmt.Errorf("query user error: %s", err.Error())
}
for _, r := range res {
r.LocalEmail = gouuid.NewV4().String() + "@fake.localhost"
err = tx.Save(&r).Error
if err != nil {
return errors.Wrap(err, "save user")
return fmt.Errorf("save column user.local_email error: %s", err)
}
}
@ -43,6 +48,11 @@ func addUserLocalEmail(db *gorm.DB) error {
}
}
err = tx.Migrator().AlterColumn(&User{}, "LocalEmail")
if err != nil {
return fmt.Errorf("alter column user.local_email error: %s", err.Error())
}
return nil
})
}

View File

@ -1,7 +1,7 @@
package migrations
import (
"github.com/pkg/errors"
"fmt"
"gorm.io/gorm"
)
@ -26,7 +26,7 @@ func insertUserPrimaryEmail(db *gorm.DB) error {
var res []User
err := tx.Table("user").Where("type = ?", 0).Offset(offset).Limit(limit).Find(&res).Error
if err != nil {
return errors.Wrap(err, "query user")
return fmt.Errorf("query user error: %s", err.Error())
}
for _, r := range res {
@ -37,7 +37,7 @@ func insertUserPrimaryEmail(db *gorm.DB) error {
}
err := tx.Table("email_address").Where("uid = ? AND email = ?", record.UserID, record.Email).FirstOrCreate(record).Error
if err != nil {
return errors.Wrap(err, "insert email")
return fmt.Errorf("insert email error: %s", err.Error())
}
}

View File

@ -1,6 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.gogs file.
// license that can be found in the LICENSE file.
package database

View File

@ -1149,6 +1149,7 @@ func (s *UsersStore) Active(ctx context.Context, userID int64) error {
user.UpdatedUnix = s.db.NowFunc().Unix()
user.Rands = rands
user.IsActive = true
err = tx.Save(user).Error
if err != nil {

View File

@ -6,6 +6,7 @@ package email
import (
"fmt"
"gogs.io/gogs/internal/tool"
"html/template"
"path/filepath"
"sync"
@ -88,7 +89,6 @@ type User interface {
DisplayName() string
Email() string
PublicEmail() string
GenerateEmailActivateCode(string) string
}
type Repository interface {
@ -123,19 +123,37 @@ func SendUserMail(_ *macaron.Context, u User, tpl, code, subject, info string) {
}
func SendActivateAccountMail(c *macaron.Context, u User) {
SendUserMail(c, u, MAIL_AUTH_ACTIVATE, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.activate_account"), "activate account")
token, err := tool.NewClaims(u.ID(), u.Email(), tool.SubjectActiveAccount).ToToken()
if err != nil {
log.Error("Create token error: %s", err.Error())
return
}
SendUserMail(c, u, MAIL_AUTH_ACTIVATE, token, c.Tr("mail.activate_account"), "activate account")
}
func SendResetPasswordMail(c *macaron.Context, u User) {
SendUserMail(c, u, MAIL_AUTH_RESET_PASSWORD, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.reset_password"), "reset password")
token, err := tool.NewClaims(u.ID(), u.Email(), tool.SubjectForgetPasswd).ToToken()
if err != nil {
log.Error("Create token error: %s", err.Error())
return
}
SendUserMail(c, u, MAIL_AUTH_RESET_PASSWORD, token, c.Tr("mail.reset_password"), "reset password")
}
// SendActivateAccountMail sends confirmation email.
func SendActivateEmailMail(c *macaron.Context, u User, email string) {
token, err := tool.NewClaims(u.ID(), email, tool.SubjectActiveEmail).ToToken()
if err != nil {
log.Error("Create token error: %s", err.Error())
return
}
data := map[string]any{
"Username": u.DisplayName(),
"ActiveCodeLives": conf.Auth.ActivateCodeLives / 60,
"Code": u.GenerateEmailActivateCode(email),
"Code": token,
"Email": email,
}
body, err := render(MAIL_AUTH_ACTIVATE_EMAIL, data)

View File

@ -6,13 +6,10 @@ package user
import (
gocontext "context"
"encoding/hex"
"fmt"
"github.com/go-macaron/captcha"
"net/http"
"net/url"
"strings"
"github.com/go-macaron/captcha"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
@ -395,66 +392,51 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
c.RedirectSubpath("/user/login")
}
// parseUserFromCode returns user by username encoded in code.
// It returns nil if code or username is invalid.
func parseUserFromCode(code string) (user *database.User) {
if len(code) <= tool.TIME_LIMIT_CODE_LENGTH {
// verify active code when active account
func verifyUserActiveCode(code string) (user *database.User) {
data, err := tool.ParseToken(code)
if err != nil || data.Valid() != nil {
return nil
}
// Use tail hex username to query user
hexStr := code[tool.TIME_LIMIT_CODE_LENGTH:]
if b, err := hex.DecodeString(hexStr); err == nil {
if user, err = database.Handle.Users().GetByUsername(gocontext.TODO(), string(b)); user != nil {
return user
} else if !database.IsErrUserNotExist(err) {
log.Error("Failed to get user by name %q: %v", string(b), err)
if user, err = database.Handle.Users().GetByID(gocontext.TODO(), data.Id); err != nil {
if !database.IsErrUserNotExist(err) {
log.Error("Failed to get user by id %d: %v", data.Id, err)
}
return nil
}
return nil
}
// verify active code when active account
func verifyUserActiveCode(code string) (user *database.User) {
minutes := conf.Auth.ActivateCodeLives
if user = parseUserFromCode(code); user != nil {
// time limit code
prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, strings.ToLower(user.Name), user.Password, user.Rands)
if tool.VerifyTimeLimitCode(data, minutes, prefix) {
return user
}
}
return nil
return user
}
// verify active code when active account
func verifyActiveEmailCode(code, email string) *database.EmailAddress {
minutes := conf.Auth.ActivateCodeLives
if user := parseUserFromCode(code); user != nil {
// time limit code
prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, strings.ToLower(user.Name), user.Password, user.Rands)
if tool.VerifyTimeLimitCode(data, minutes, prefix) {
emailAddress, err := database.Handle.Users().GetEmail(gocontext.TODO(), user.ID, email, false)
if err == nil {
return emailAddress
}
}
data, err := tool.ParseToken(code)
if err != nil {
return nil
} else if data.Valid() != nil {
return nil
}
return nil
user, err := database.Handle.Users().GetByID(gocontext.TODO(), data.Id)
if err != nil || user == nil {
log.Error("Failed to get user by id %d: %v", data.Id, err)
return nil
}
emailAddress, err := database.Handle.Users().GetEmail(gocontext.TODO(), user.ID, email, false)
if err != nil {
return nil
}
return emailAddress
}
func Activate(c *context.Context) {
code := c.Query("code")
if code == "" {
c.Data["IsActivatePage"] = true
if c.User.IsActive {
if c.User == nil || c.User.IsActive {
c.NotFound()
return
}
@ -602,25 +584,31 @@ func ResetPasswdPost(c *context.Context) {
}
c.Data["Code"] = code
if u := verifyUserActiveCode(code); u != nil {
// Validate password length.
password := c.Query("password")
if len(password) < 6 {
c.Data["IsResetForm"] = true
c.Data["Err_Password"] = true
c.RenderWithErr(c.Tr("auth.password_too_short"), RESET_PASSWORD, nil)
return
}
data, err := tool.ParseToken(code)
if err == nil && data.Valid() == nil {
user, err := database.Handle.Users().GetByID(gocontext.TODO(), data.Id)
if err == nil && user != nil {
// Validate password length.
password := c.Query("password")
if len(password) < 6 {
c.Data["IsResetForm"] = true
c.Data["Err_Password"] = true
c.RenderWithErr(c.Tr("auth.password_too_short"), RESET_PASSWORD, nil)
return
}
err := database.Handle.Users().Update(c.Req.Context(), u.ID, database.UpdateUserOptions{Password: &password})
if err != nil {
c.Error(err, "update user")
return
}
err := database.Handle.Users().Update(c.Req.Context(), user.ID, database.UpdateUserOptions{Password: &password})
if err != nil {
c.Error(err, "update user")
return
}
log.Trace("User password reset: %s", u.Name)
c.RedirectSubpath("/user/login")
return
log.Trace("User password reset: %s", user.Name)
c.RedirectSubpath("/user/login")
return
} else if user == nil {
log.Error("Failed to get user by id %d: %v", data.Id, err)
}
}
c.Data["IsResetFailed"] = true

102
internal/tool/jwt.go Normal file
View File

@ -0,0 +1,102 @@
package tool
import (
"crypto/rand"
"fmt"
"github.com/dgrijalva/jwt-go"
"gogs.io/gogs/internal/conf"
"time"
)
type Subject int
const (
SubjectActiveAccount Subject = 1
SubjectActiveEmail Subject = 2
SubjectForgetPasswd Subject = 3
)
var secretKey = make([]byte, 32)
func init() {
if _, err := rand.Read(secretKey); err != nil {
panic(err)
}
}
type Claims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id int64 `json:"jti,omitempty"`
Email string `json:"email,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject Subject `json:"sub,omitempty"`
}
func (c *Claims) Valid() error {
now := time.Now()
if now.After(time.Unix(c.ExpiresAt, 0)) {
return fmt.Errorf("error")
}
if now.Before(time.Unix(c.NotBefore, 0)) {
return fmt.Errorf("error")
}
if now.Before(time.Unix(c.IssuedAt, 0)) {
return fmt.Errorf("error")
}
if c.Audience != c.Email {
return fmt.Errorf("error")
}
return nil
}
func NewClaims(id int64, email string, subject Subject) *Claims {
now := time.Now()
return &Claims{
Audience: email,
ExpiresAt: now.Add(time.Duration(conf.Auth.ActivateCodeLives) * time.Minute).Unix(),
Id: id,
Email: email,
IssuedAt: now.Unix(),
Issuer: conf.Server.ExternalURL,
NotBefore: now.Unix(),
Subject: subject,
}
}
func (c *Claims) ToToken() (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
//使用指定的secret签名并获得完成的编码后的字符串token
return token.SignedString(secretKey)
}
func ParseToken(t string) (*Claims, error) {
//解析token
token, err := jwt.ParseWithClaims(t, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
return secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && claims != nil && token.Valid {
return claims, nil
} else if err := claims.Valid(); err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && claims != nil && token.Valid {
if err := claims.Valid(); err != nil {
return nil, err
}
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}

View File

@ -5,9 +5,7 @@
package tool
import (
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"html/template"
"strings"
@ -62,66 +60,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
return auth[0], auth[1], nil
}
// verify time limit code
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
if len(code) <= 18 {
return false
}
// split code
start := code[:12]
lives := code[12:18]
if d, err := com.StrTo(lives).Int(); err == nil {
minutes = d
}
// right active code
retCode := CreateTimeLimitCode(data, minutes, start)
if retCode == code && minutes > 0 {
// check time is expired or not
before, _ := time.ParseInLocation("200601021504", start, time.Local)
now := time.Now()
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
return true
}
}
return false
}
const TIME_LIMIT_CODE_LENGTH = 12 + 6 + 40
// CreateTimeLimitCode generates a time limit code based on given input data.
// Format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
func CreateTimeLimitCode(data string, minutes int, startInf any) string {
format := "200601021504"
var start, end time.Time
var startStr, endStr string
if startInf == nil {
// Use now time create code
start = time.Now()
startStr = start.Format(format)
} else {
// use start string create code
startStr = startInf.(string)
start, _ = time.ParseInLocation(format, startStr, time.Local)
startStr = start.Format(format)
}
end = start.Add(time.Minute * time.Duration(minutes))
endStr = end.Format(format)
// create sha1 encode string
sh := sha1.New()
_, _ = sh.Write([]byte(data + conf.Security.SecretKey + startStr + endStr + com.ToStr(minutes)))
encoded := hex.EncodeToString(sh.Sum(nil))
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
return code
}
// HashEmail hashes email address to MD5 string.
// https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {

View File

@ -8,23 +8,19 @@ import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"github.com/nfnt/resize"
"github.com/pkg/errors"
"golang.org/x/crypto/pbkdf2"
"image"
"image/png"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/nfnt/resize"
"github.com/pkg/errors"
"golang.org/x/crypto/pbkdf2"
"gogs.io/gogs/internal/avatar"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/strutil"
"gogs.io/gogs/internal/tool"
)
// DashboardURLPath returns the URL path to the user or organization dashboard.
@ -35,20 +31,6 @@ func DashboardURLPath(name string, isOrganization bool) string {
return conf.Server.Subpath + "/"
}
// GenerateActivateCode generates an activate code based on user information and
// the given email.
func GenerateActivateCode(userID int64, email, name, password, rands string) string {
code := tool.CreateTimeLimitCode(
fmt.Sprintf("%d%s%s%s%s", userID, email, strings.ToLower(name), password, rands),
conf.Auth.ActivateCodeLives,
nil,
)
// Add tailing hex username
code += hex.EncodeToString([]byte(strings.ToLower(name)))
return code
}
// CustomAvatarPath returns the absolute path of the user custom avatar file.
func CustomAvatarPath(userID int64) string {
return filepath.Join(conf.Picture.AvatarUploadPath, strconv.FormatInt(userID, 10))

View File

@ -14,7 +14,6 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/tool"
"gogs.io/gogs/public"
)
@ -32,18 +31,6 @@ func TestDashboardURLPath(t *testing.T) {
})
}
func TestGenerateActivateCode(t *testing.T) {
conf.SetMockAuth(t,
conf.AuthOpts{
ActivateCodeLives: 10,
},
)
code := GenerateActivateCode(1, "alice@example.com", "Alice", "123456", "rands")
got := tool.VerifyTimeLimitCode("1alice@example.comalice123456rands", conf.Auth.ActivateCodeLives, code[:tool.TIME_LIMIT_CODE_LENGTH])
assert.True(t, got)
}
func TestCustomAvatarPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -5,7 +5,7 @@
<div class="ui middle very relaxed page grid">
<div class="sixteen wide center aligned centered column">
<div class="explore-logo">
<img alt="logo" src="{{AppSubURL}}/img/logo-text.png" />
<img alt="logo" src="{{AppSubURL}}/img/gogs-hero.png" />
</div>
<div class="export-hero">
<h1>

View File

@ -3,7 +3,7 @@
<div class="ui stackable middle very relaxed page grid">
<div class="sixteen wide center aligned centered column">
<div class="logo">
<img alt="logo" src="{{AppSubURL}}/img/logo-text.png" />
<img alt="logo" src="{{AppSubURL}}/img/gogs-hero.png" />
</div>
<div class="hero">
<h2>

View File

@ -4,7 +4,7 @@
<div class="ui stackable middle very relaxed page grid">
<div class="sixteen wide center aligned centered column">
<div class="dashboard-logo">
<img alt="logo" src="{{AppSubURL}}/img/logo-text.png" />
<img alt="logo" src="{{AppSubURL}}/img/gogs-hero.png" />
</div>
<div class="dashboard-hero">
<h1>