Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ build_backend_on_darwin:

build_all: build_frontend build_backend_on_linux

build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin
build_on_local: clean_assets build_frontend build_backend_on_darwin
23 changes: 17 additions & 6 deletions backend/app/api/v1/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import (
"encoding/base64"

"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/captcha"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin"
)

Expand All @@ -26,7 +28,9 @@
return
}

if req.AuthMethod != "jwt" && !req.IgnoreCaptcha {
ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
if needCaptcha {
if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
Expand All @@ -48,9 +52,11 @@
user, err := authService.Login(c, req, string(entrance))
go saveLoginLogs(c, err)
if err != nil {
global.IPTracker.SetNeedCaptcha(ip)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
global.IPTracker.Clear(ip)
helper.SuccessWithData(c, user)
}

Expand Down Expand Up @@ -134,16 +140,21 @@
}

// @Tags Auth
// @Summary Load System Language
// @Success 200 {string} language
// @Router /auth/language [get]
func (b *BaseApi) GetLanguage(c *gin.Context) {
// @Summary Load System Setting for login
// @Success 200 {object} dto.LoginSetting
// @Router /auth/setting [get]
func (b *BaseApi) GetAuthSetting(c *gin.Context) {

Check warning on line 146 in backend/app/api/v1/auth.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the 'Get' prefix from this function name.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZroz-Mu_hlLf-3SRbmt&open=AZroz-Mu_hlLf-3SRbmt&pullRequest=11187
settingInfo, err := settingService.GetSettingInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, settingInfo.Language)
ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
helper.SuccessWithData(c, dto.LoginSetting{
NeedCaptcha: needCaptcha,
Language: settingInfo.Language,
})
}

func saveLoginLogs(c *gin.Context, err error) {
Expand Down
18 changes: 11 additions & 7 deletions backend/app/dto/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ type MfaCredential struct {
}

type Login struct {
Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"`
IgnoreCaptcha bool `json:"ignoreCaptcha"`
Captcha string `json:"captcha"`
CaptchaID string `json:"captchaID"`
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"`
Captcha string `json:"captcha"`
CaptchaID string `json:"captchaID"`
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
}

type MFALogin struct {
Expand All @@ -38,3 +37,8 @@ type MFALogin struct {
Code string `json:"code" validate:"required"`
AuthMethod string `json:"authMethod"`
}

type LoginSetting struct {
NeedCaptcha bool `json:"needCaptcha"`
Language string `json:"language"`
}
3 changes: 3 additions & 0 deletions backend/global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package global

import (
"github.com/1Panel-dev/1Panel/backend/configs"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
"github.com/1Panel-dev/1Panel/backend/init/session/psession"
"github.com/dgraph-io/badger/v4"
Expand All @@ -28,6 +29,8 @@ var (
MonitorCronID cron.EntryID
OneDriveCronID cron.EntryID

IPTracker *auth.IPTracker

I18n *i18n.Localizer
I18nForCmd *i18n.Localizer
)
99 changes: 99 additions & 0 deletions backend/init/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package auth

import (
"sync"
"time"
)

const (
MaxIPCount = 100
ExpireDuration = 30 * time.Minute
)

type IPRecord struct {
NeedCaptcha bool
LastUpdate time.Time
}

type IPTracker struct {
records map[string]*IPRecord
ipOrder []string
mu sync.RWMutex
}

func NewIPTracker() *IPTracker {
return &IPTracker{
records: make(map[string]*IPRecord),
ipOrder: make([]string, 0),
}
}

func (t *IPTracker) NeedCaptcha(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()

record, exists := t.records[ip]
if !exists {
return false
}

if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
return false
}

return record.NeedCaptcha
}

func (t *IPTracker) SetNeedCaptcha(ip string) {
t.mu.Lock()
defer t.mu.Unlock()

if record, exists := t.records[ip]; exists {
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
} else {
record.NeedCaptcha = true
record.LastUpdate = time.Now()
return
}
}

if len(t.records) >= MaxIPCount {
t.removeOldestUnsafe()
}

t.records[ip] = &IPRecord{
NeedCaptcha: true,
LastUpdate: time.Now(),
}
t.ipOrder = append(t.ipOrder, ip)
}

func (t *IPTracker) Clear(ip string) {
t.mu.Lock()
defer t.mu.Unlock()

t.removeIPUnsafe(ip)
}

func (t *IPTracker) removeIPUnsafe(ip string) {
delete(t.records, ip)

for i, storedIP := range t.ipOrder {
if storedIP == ip {
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
break
}
}
}

func (t *IPTracker) removeOldestUnsafe() {
if len(t.ipOrder) == 0 {
return
}

oldestIP := t.ipOrder[0]
delete(t.records, oldestIP)
t.ipOrder = t.ipOrder[1:]
}
2 changes: 1 addition & 1 deletion backend/router/ro_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) {
baseRouter.POST("/login", baseApi.Login)
baseRouter.POST("/logout", baseApi.LogOut)
baseRouter.GET("/demo", baseApi.CheckIsDemo)
baseRouter.GET("/language", baseApi.GetLanguage)
baseRouter.GET("/setting", baseApi.GetAuthSetting)
baseRouter.GET("/intl", baseApi.CheckIsIntl)
}
}
2 changes: 2 additions & 0 deletions backend/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/i18n"

"github.com/1Panel-dev/1Panel/backend/init/app"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/business"
"github.com/1Panel-dev/1Panel/backend/init/lang"

Expand Down Expand Up @@ -52,6 +53,7 @@ func Start() {
business.Init()

rootRouter := router.Routers()
global.IPTracker = auth.NewIPTracker()

tcpItem := "tcp4"
if global.CONF.System.Ipv6 == "enable" {
Expand Down
6 changes: 3 additions & 3 deletions backend/utils/captcha/captcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
var store = base64Captcha.DefaultMemStore

func VerifyCode(codeID string, code string) error {
if codeID == "" {
return constant.ErrCaptchaCode
}
vv := store.Get(codeID, true)
vv = strings.TrimSpace(vv)
code = strings.TrimSpace(code)

if codeID == "" || code == "" {
return constant.ErrCaptchaCode
}
if strings.EqualFold(vv, code) {
return nil
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/api/interface/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export namespace Login {
export interface ReqLoginForm {
name: string;
password: string;
ignoreCaptcha: boolean;
captcha: string;
captchaID: string;
authMethod: string;
Expand All @@ -26,4 +25,8 @@ export namespace Login {
export interface ResAuthButtons {
[propName: string]: any;
}
export interface LoginSetting {
language: string;
needCaptcha: boolean;
}
}
4 changes: 2 additions & 2 deletions frontend/src/api/modules/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const checkIsDemo = () => {
return http.get<boolean>('/auth/demo');
};

export const getLanguage = () => {
return http.get<string>(`/auth/language`);
export const getAuthSetting = () => {
return http.get<Login.LoginSetting>(`/auth/setting`);
};

export const checkIsIntl = () => {
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/views/login/components/login-form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
import { ref, reactive, onMounted, computed, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import type { ElForm } from 'element-plus';
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getLanguage, checkIsIntl } from '@/api/modules/auth';
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getAuthSetting, checkIsIntl } from '@/api/modules/auth';
import { GlobalStore, MenuStore, TabsStore } from '@/store';
import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n';
Expand Down Expand Up @@ -318,7 +318,6 @@ const login = (formEl: FormInstance | undefined) => {
let requestLoginForm = {
name: loginForm.name,
password: encryptPassword(loginForm.password),
ignoreCaptcha: globalStore.ignoreCaptcha,
captcha: loginForm.captcha,
captchaID: captcha.captchaID,
authMethod: 'session',
Expand Down Expand Up @@ -400,11 +399,12 @@ const checkIsSystemDemo = async () => {
isDemo.value = res.data;
};

const loadLanguage = async () => {
const loadLoginSetting = async () => {
try {
const res = await getLanguage();
loginForm.language = res.data;
handleCommand(res.data);
const res = await getAuthSetting();
loginForm.language = res.data.language;
globalStore.ignoreCaptcha = !res.data.needCaptcha;
handleCommand(loginForm.language);
} catch (error) {}
};

Expand All @@ -426,7 +426,7 @@ onMounted(() => {
globalStore.isOnRestart = false;
checkIsSystemIntl();
loginVerify();
loadLanguage();
loadLoginSetting();
document.title = globalStore.themeConfig.panelName;
loginForm.agreeLicense = globalStore.agreeLicense;
checkIsSystemDemo();
Expand Down
Loading