Compare commits

...

9 Commits

Author SHA1 Message Date
jmiller af1c6178ef Merge pull request 'fix: http content file render (#207)' (#212) from fix/207-http-content-render into dev 2026-05-26 17:55:43 +00:00
Giteabot 0f23219ee4 fix: http content file render (#37850) (#37856)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Backport #37850

Fix #37849

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
2026-05-26 12:54:37 -05:00
jmiller bc475c91f6 Merge pull request 'feat: login notification via email and ntfy' (#209) from feat/login-notifications into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 21s
2026-05-26 16:40:51 +00:00
Jonathan Miller 25268d7dd7 feat: login notification via email and ntfy on successful sign-in
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
When a user signs in, sends notifications with username, IP address,
user agent, and timestamp. Notifications go through:
- Email to the user's registered address
- ntfy push to the configured topic

Enabled by default, configurable via app.ini:
  [login_notification]
  ENABLED = true

The notification fires asynchronously (goroutine) so it doesn't
block the login redirect. Hooks into handleSignInFull which is the
single choke point for all auth methods (password, 2FA, OAuth).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 11:39:44 -05:00
jmiller c7193abc0c Merge pull request 'fix: help link in footer, login logo on signin page' (#205) from fix/help-footer-login-logo into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 22s
2026-05-26 04:52:00 +00:00
Jonathan Miller e6a4dfccf0 fix(ui): add help link to footer, show login logo on signin page
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
- Help link added to footer right-links (next to API and Licenses)
- Login logo (login-logo.png) now shown on the signin page, not just
  the home page. Hidden via onerror when not uploaded.
- Landing page is set to 'login' so home.tmpl never renders — the
  logo needed to be on signin_inner.tmpl instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 23:51:37 -05:00
jmiller a5c805b0f6 Merge pull request 'fix(ui): show help link for all users' (#204) from fix/help-link-always-visible into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 24s
2026-05-26 04:45:56 +00:00
Jonathan Miller 46ce0a7e32 fix(ui): show help link in navbar for all users, not just anonymous
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The help link was wrapped in {{if not .IsSigned}} which hid it for
logged-in users. Removed the condition so it's always visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 23:45:49 -05:00
jmiller c236c4e018 Merge pull request 'fix(ui): replace missing octicon-dashboard icon' (#202) from fix/missing-dashboard-icon into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 20s
2026-05-26 04:39:00 +00:00
8 changed files with 123 additions and 9 deletions
+14 -2
View File
@@ -63,7 +63,7 @@ func TestFile(t *testing.T) {
{
name: "tags.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
want: lines(`<span class="o">&lt;&gt;</span>`),
lexerName: "Python",
},
{
@@ -102,7 +102,7 @@ c=2
<span class="n">def</span><span class="p">:</span>\n
<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n
\n
<span class="n">b</span><span class="o">=</span><span class="sa"></span><span class="s1">&#39;</span><span class="s1">&#39;</span>\n
<span class="n">b</span><span class="o">=</span><span class="s1">&#39;&#39;</span>\n
\n
<span class="n">c</span><span class="o">=</span><span class="mi">2</span>`,
),
@@ -114,6 +114,18 @@ c=2
want: []template.HTML{"<span class=\"c1\">--\n</span>", `<span class="k">SELECT</span>`},
lexerName: "SQL",
},
{
name: "test.http",
code: `HTTP/1.0 400 Bad request
Content-Type: text/html
<html></html>`,
want: lines(`<span class="kr">HTTP</span><span class="o">/</span><span class="m">1.0</span> <span class="m">400</span> <span class="ne">Bad request</span>\n
<span class="n">Content-Type</span><span class="o">:</span> <span class="l">text/html</span>\n
\n
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>`),
lexerName: "HTTP",
},
}
for _, tt := range tests {
+4 -4
View File
@@ -288,24 +288,24 @@ func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lex
// if lang is provided, and it matches a lexer, use it directly
if byLang {
return lexer
return chroma.Coalesce(lexer)
}
// if a lexer is detected and there is no conflict for the file extension, use it directly
fileExt := path.Ext(fileName)
_, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt]
if !hasConflicts && lexer != lexers.Fallback {
return lexer
return chroma.Coalesce(lexer)
}
// try to detect language by content, for best guessing for the language
// when using "code" to detect, analyze.GetCodeLanguage is slow, it iterates many rules to detect language from content
analyzedLanguage := analyze.GetCodeLanguage(fileName, code)
lexer = DetectChromaLexerByFileName(fileName, analyzedLanguage)
lexer, _ = detectChromaLexerByFileName(fileName, analyzedLanguage)
if lexer == lexers.Fallback {
if analyzedLanguage != enry.OtherLanguage {
log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName)
}
}
return lexer
return chroma.Coalesce(lexer)
}
+13
View File
@@ -39,6 +39,13 @@ var (
Channel: "stable",
}
// LoginNotification configuration for sign-in alerts
LoginNotification = struct {
Enabled bool
}{
Enabled: true,
}
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
@@ -171,6 +178,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadOtherFrom(cfg)
loadUpdateCheckerFrom(cfg)
loadNtfyFrom(cfg)
loadLoginNotificationFrom(cfg)
return nil
}
@@ -181,6 +189,11 @@ func loadUpdateCheckerFrom(cfg ConfigProvider) {
UpdateChecker.Channel = sec.Key("CHANNEL").MustString(UpdateChecker.Channel)
}
func loadLoginNotificationFrom(cfg ConfigProvider) {
sec := cfg.Section("login_notification")
LoginNotification.Enabled = sec.Key("ENABLED").MustBool(true)
}
func loadRunModeFrom(rootCfg ConfigProvider) {
rootSec := rootCfg.Section("")
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
+3
View File
@@ -440,6 +440,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
ctx.ServerError("UpdateUser", err)
return
}
// Send login notification (email + ntfy)
go mailer.SendLoginNotification(u, ctx.RemoteAddr(), ctx.Req.UserAgent())
}
// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package mailer
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
sender_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer/sender"
)
// SendLoginNotification sends email and ntfy notifications when a user signs in.
func SendLoginNotification(u *user_model.User, ip, userAgent string) {
if !setting.LoginNotification.Enabled {
return
}
timestamp := time.Now().UTC().Format("2006-01-02 15:04:05 UTC")
subject := fmt.Sprintf("[%s] New sign-in: %s", setting.AppName, u.Name)
body := fmt.Sprintf(`New sign-in detected
Account: %s (%s)
IP Address: %s
Browser: %s
Time: %s
Instance: %s
If this wasn't you, change your password immediately and review your active sessions.
— %s`, u.Name, u.Email, ip, userAgent, timestamp, setting.AppURL, setting.AppName)
// Email notification
if setting.MailService != nil && u.Email != "" {
msg := sender_service.NewMessage(u.EmailTo(), subject, body)
msg.Info = fmt.Sprintf("Login notification for %s", u.Name)
SendAsync(msg)
log.Debug("Login notification email sent to %s", u.Email)
}
// ntfy push notification
if setting.Ntfy.Enabled && setting.Ntfy.ServerURL != "" {
go sendLoginNtfy(subject, u.Name, ip, timestamp)
}
}
func sendLoginNtfy(title, username, ip, timestamp string) {
body := fmt.Sprintf("User: %s\nIP: %s\nTime: %s", username, ip, timestamp)
url := fmt.Sprintf("%s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
req, err := http.NewRequest("POST", url, bytes.NewBufferString(body))
if err != nil {
log.Error("ntfy login: create request: %v", err)
return
}
req.Header.Set("Title", title)
req.Header.Set("Priority", "default")
req.Header.Set("Tags", "key,login")
req.Header.Set("Click", setting.AppURL+"-/admin")
if setting.Ntfy.Token != "" {
req.Header.Set("Authorization", "Bearer "+setting.Ntfy.Token)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Error("ntfy login: send: %v", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 300 {
log.Error("ntfy login: status %d", resp.StatusCode)
}
}
+1
View File
@@ -36,6 +36,7 @@
</div>
<a href="{{AssetUrlPrefix}}/licenses.txt">{{ctx.Locale.Tr "licenses"}}</a>
{{if .EnableSwagger}}<a href="{{AppSubUrl}}/api/swagger">API</a>{{end}}
<a href="{{HelpURL}}" target="_blank">{{ctx.Locale.Tr "help"}}</a>
{{template "custom/extra_links_footer" .}}
</div>
</footer>
+1 -3
View File
@@ -35,9 +35,7 @@
{{template "custom/extra_links" .}}
{{if not .IsSigned}}
<a class="item" target="_blank" href="{{HelpURL}}">{{ctx.Locale.Tr "help"}}</a>
{{end}}
<a class="item" target="_blank" href="{{HelpURL}}">{{ctx.Locale.Tr "help"}}</a>
</div>
<!-- the full dropdown menus -->
+3
View File
@@ -1,4 +1,7 @@
<div class="ui container fluid">
<div class="tw-text-center tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/login-logo.png" style="max-width: 220px; max-height: 80px; object-fit: contain;" onerror="this.style.display='none'">
</div>
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
{{template "base/alert" .}}
{{end}}