Compare commits

..

1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7d755e5376 Initial plan 2026-01-30 01:35:39 +00:00
16 changed files with 77 additions and 344 deletions

View File

@@ -51,14 +51,22 @@ COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
ENTRYPOINT ["/server"]
FROM debian:latest AS ALLINONE
FROM debian:latest AS db
RUN apt update \
&& apt install -y \
mariadb-server \
mariadb-client \
&& rm -rf /var/lib/apt/lists/*
FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update
RUN apt install -y ca-certificates lsof && update-ca-certificates
RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server

View File

@@ -1,10 +1,8 @@
#!/bin/bash
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
if [ -z "${driverName:-}" ]; then
export driverName=sqlite
fi
if [ -z "${dataSourceName:-}" ]; then
export dataSourceName="file:casdoor.db?cache=shared"
fi
service mariadb start
exec /server
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
exec /server --createDatabase=true

View File

@@ -216,16 +216,6 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
e.AddAttribute("sn", message.AttributeValue(user.LastName))
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
// Add POSIX attributes for Linux machine login support
e.AddAttribute("loginShell", getAttribute("loginShell", user))
e.AddAttribute("gecos", getAttribute("gecos", user))
// Add SSH public key if available
sshKey := getAttribute("sshPublicKey", user)
if sshKey != "" {
e.AddAttribute("sshPublicKey", sshKey)
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}

View File

@@ -83,45 +83,6 @@ var ldapAttributesMapping = map[string]FieldRelation{
return message.AttributeValue(getUserPasswordWithType(user))
},
},
"loginShell": {
userField: "loginShell",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Check user properties first, otherwise return default shell
if user.Properties != nil {
if shell, ok := user.Properties["loginShell"]; ok && shell != "" {
return message.AttributeValue(shell)
}
}
return message.AttributeValue("/bin/bash")
},
},
"gecos": {
userField: "gecos",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// GECOS field typically contains full name and other user info
// Format: Full Name,Room Number,Work Phone,Home Phone,Other
gecos := user.DisplayName
if gecos == "" {
gecos = user.Name
}
return message.AttributeValue(gecos)
},
},
"sshPublicKey": {
userField: "sshPublicKey",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Return SSH public key from user properties
if user.Properties != nil {
if sshKey, ok := user.Properties["sshPublicKey"]; ok && sshKey != "" {
return message.AttributeValue(sshKey)
}
}
return message.AttributeValue("")
},
},
}
const ldapMemberOfAttr = "memberOf"

View File

@@ -32,7 +32,6 @@ type AccountItem struct {
ViewRule string `json:"viewRule"`
ModifyRule string `json:"modifyRule"`
Regex string `json:"regex"`
Tab string `json:"tab"`
}
type ThemeData struct {
@@ -89,7 +88,6 @@ type Organization struct {
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"`
AccountMenu string `xorm:"varchar(20)" json:"accountMenu"`
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
OrgBalance float64 `json:"orgBalance"`

View File

@@ -50,8 +50,7 @@ type Payment struct {
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
// Order Info
Order string `xorm:"varchar(100)" json:"order"` // Internal order name
OrderObj *Order `xorm:"-" json:"orderObj,omitempty"`
Order string `xorm:"varchar(100)" json:"order"` // Internal order name
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"` // External payment provider's order ID
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl"` // `successUrl` is redirected from `payUrl` after pay success
@@ -71,11 +70,6 @@ func GetPayments(owner string) ([]*Payment, error) {
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
@@ -86,11 +80,6 @@ func GetUserPayments(owner, user string) ([]*Payment, error) {
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
@@ -102,49 +91,9 @@ func GetPaginationPayments(owner string, offset, limit int, field, value, sortFi
return nil, err
}
err = ExtendPaymentWithOrder(payments)
if err != nil {
return nil, err
}
return payments, nil
}
func ExtendPaymentWithOrder(payments []*Payment) error {
ownerOrdersMap := make(map[string][]string)
for _, payment := range payments {
if payment.Order != "" {
ownerOrdersMap[payment.Owner] = append(ownerOrdersMap[payment.Owner], payment.Order)
}
}
ordersMap := make(map[string]*Order)
for owner, orderNames := range ownerOrdersMap {
if len(orderNames) == 0 {
continue
}
var orders []*Order
err := ormer.Engine.In("name", orderNames).Find(&orders, &Order{Owner: owner})
if err != nil {
return err
}
for _, order := range orders {
ordersMap[util.GetId(order.Owner, order.Name)] = order
}
}
for _, payment := range payments {
if payment.Order != "" {
orderId := util.GetId(payment.Owner, payment.Order)
if order, ok := ordersMap[orderId]; ok {
payment.OrderObj = order
}
}
}
return nil
}
func getPayment(owner string, name string) (*Payment, error) {
if owner == "" || name == "" {
return nil, nil
@@ -261,29 +210,18 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
}
// Check if payment is already in a terminal state to prevent duplicate processing
if pp.IsTerminalState(payment.State) {
if payment.State == pp.PaymentStatePaid || payment.State == pp.PaymentStateError ||
payment.State == pp.PaymentStateCanceled || payment.State == pp.PaymentStateTimeout {
return payment, nil
}
// Determine the new payment state
var newState pp.PaymentState
var newMessage string
if err != nil {
newState = pp.PaymentStateError
newMessage = err.Error()
payment.State = pp.PaymentStateError
payment.Message = err.Error()
} else {
newState = notifyResult.PaymentStatus
newMessage = notifyResult.NotifyMessage
payment.State = notifyResult.PaymentStatus
payment.Message = notifyResult.NotifyMessage
}
// Check if the payment state would actually change
// This prevents duplicate webhook events when providers send redundant notifications
if payment.State == newState {
return payment, nil
}
payment.State = newState
payment.Message = newMessage
_, err = UpdatePayment(payment.GetId(), payment)
if err != nil {
return nil, err

View File

@@ -918,8 +918,6 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
err = setReflectAttr[[]MfaAccount](&fv, v)
case reflect.TypeOf([]webauthn.Credential{}):
err = setReflectAttr[[]webauthn.Credential](&fv, v)
case reflect.TypeOf(map[string]string{}):
err = setReflectAttr[map[string]string](&fv, v)
}
if err != nil {

View File

@@ -24,12 +24,6 @@ const (
PaymentStateError PaymentState = "Error"
)
// IsTerminalState checks if a payment state is terminal (cannot transition to other states)
func IsTerminalState(state PaymentState) bool {
return state == PaymentStatePaid || state == PaymentStateError ||
state == PaymentStateCanceled || state == PaymentStateTimeout
}
const (
PaymentEnvWechatBrowser = "WechatBrowser"
)

View File

@@ -144,8 +144,8 @@ class OrderListPage extends BaseListPage {
key: "products",
...this.getColumnSearchProps("products"),
render: (text, record, index) => {
const productInfos = record?.productInfos || [];
if (productInfos.length === 0) {
const products = record?.products || [];
if (products.length === 0) {
return `(${i18next.t("general:empty")})`;
}
return (
@@ -153,26 +153,21 @@ class OrderListPage extends BaseListPage {
<List
size="small"
locale={{emptyText: " "}}
dataSource={productInfos}
dataSource={products}
style={{
paddingTop: 8,
paddingBottom: 8,
}}
renderItem={(productInfo, i) => {
const price = productInfo.price * (productInfo.quantity || 1);
const currency = record.currency || "USD";
renderItem={(productName, i) => {
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
</Tooltip>
<Link to={`/products/${record.owner}/${productInfo.name}`}>
{productInfo.displayName || productInfo.name}
<Link to={`/products/${record.owner}/${productName}`}>
{productName}
</Link>
<span style={{marginLeft: "8px", color: "#666"}}>
{Setting.getPriceDisplay(price, currency)}
</span>
</div>
</List.Item>
);

View File

@@ -678,16 +678,6 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account menu"), i18next.t("organization:Account menu - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.accountMenu || "Horizontal"} onChange={(value => {this.updateOrganizationField("accountMenu", value);})}
options={[{value: "Horizontal", label: i18next.t("general:Horizontal")}, {value: "Vertical", label: i18next.t("general:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@@ -180,8 +180,8 @@ class PaymentListPage extends BaseListPage {
key: "products",
...this.getColumnSearchProps("products"),
render: (text, record, index) => {
const productInfos = record?.orderObj?.productInfos || [];
if (productInfos.length === 0) {
const products = record?.products || [];
if (products.length === 0) {
return `(${i18next.t("general:empty")})`;
}
return (
@@ -189,26 +189,21 @@ class PaymentListPage extends BaseListPage {
<List
size="small"
locale={{emptyText: " "}}
dataSource={productInfos}
dataSource={products}
style={{
paddingTop: 8,
paddingBottom: 8,
}}
renderItem={(productInfo, i) => {
const price = productInfo.price * (productInfo.quantity || 1);
const currency = record.currency || "USD";
renderItem={(productName, i) => {
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
</Tooltip>
<Link to={`/products/${record.owner}/${productInfo.name}`}>
{productInfo.displayName || productInfo.name}
<Link to={`/products/${record.owner}/${productName}`}>
{productName}
</Link>
<span style={{marginLeft: "8px", color: "#666"}}>
{Setting.getPriceDisplay(price, currency)}
</span>
</div>
</List.Item>
);

View File

@@ -340,7 +340,7 @@ class ProductBuyPage extends React.Component {
{i18next.t("order:Place Order")}
</Button>
<Button
type="default"
type="primary"
size="large"
style={{
height: "50px",

View File

@@ -162,7 +162,7 @@ class ProductStorePage extends React.Component {
{!product.isRecharge && (
<Button
key="add"
type="default"
type="primary"
onClick={(e) => {
e.stopPropagation();
this.addToCart(product);

View File

@@ -13,10 +13,7 @@
// limitations under the License.
import React from "react";
import {
Button, Card, Col, Form, Input, InputNumber, Layout, List,
Menu, Result, Row, Select, Space, Spin, Switch, Tabs, Tag, Tooltip
} from "antd";
import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
import {withRouter} from "react-router-dom";
import {TotpMfaType} from "./auth/MfaSetupPage";
import * as GroupBackend from "./backend/GroupBackend";
@@ -49,8 +46,6 @@ import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
import TransactionTable from "./table/TransactionTable";
import * as TransactionBackend from "./backend/TransactionBackend";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
const {Option} = Select;
@@ -72,8 +67,6 @@ class UserEditPage extends React.Component {
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
openFaceRecognitionModal: false,
transactions: [],
activeMenuKey: window.location.hash?.slice(1) || "",
menuMode: "Horizontal",
};
}
@@ -182,7 +175,6 @@ class UserEditPage extends React.Component {
}
this.setState({
menuMode: res.data?.organizationObj?.accountMenu ?? "Horizontal",
application: res.data,
});
});
@@ -1341,152 +1333,6 @@ class UserEditPage extends React.Component {
);
}
isAccountItemVisible(item) {
if (!item.visible) {
return false;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (item.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return false;
}
} else if (item.viewRule === "Admin") {
if (!isAdmin) {
return false;
}
}
return true;
}
getAccountItemsByTab(tab) {
const accountItems = this.getUserOrganization()?.accountItems || [];
return accountItems.filter(item => {
if (!this.isAccountItemVisible(item)) {
return false;
}
const itemTab = item.tab || "";
return itemTab === tab;
});
}
getUniqueTabs() {
const accountItems = this.getUserOrganization()?.accountItems || [];
const tabs = new Set();
accountItems.forEach(item => {
if (this.isAccountItemVisible(item)) {
tabs.add(item.tab || "");
}
});
return Array.from(tabs).sort((a, b) => {
// Empty string (default tab) comes first
if (a === "") {
return -1;
}
if (b === "") {
return 1;
}
return a.localeCompare(b);
});
}
renderUserForm() {
const tabs = this.getUniqueTabs();
// If there are no tabs or only one tab (default), render without tab navigation
if (tabs.length === 0 || (tabs.length === 1 && tabs[0] === "")) {
const accountItems = this.getAccountItemsByTab("");
return (
<Form>
{accountItems.map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
);
}
// Render with tabs
const activeKey = this.state.activeMenuKey || tabs[0] || "";
return (
<Layout style={{background: "inherit"}}>
{
this.state.menuMode === "Vertical" ? null : (
<Header style={{background: "inherit", padding: "0px"}}>
<Tabs
onChange={(key) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
type="card"
activeKey={activeKey}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Header>
)
}
<Layout style={{background: "inherit", maxHeight: "70vh", overflow: "auto"}}>
{
this.state.menuMode === "Vertical" ? (
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
<Menu
mode="vertical"
selectedKeys={[activeKey]}
onClick={({key}) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
style={{marginBottom: "20px", height: "100%"}}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Sider>) : null
}
<Content style={{padding: "15px"}}>
<Form>
{this.getAccountItemsByTab(activeKey).map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
</Content>
</Layout>
</Layout>
);
}
renderUser() {
return (
<div>
@@ -1500,7 +1346,42 @@ class UserEditPage extends React.Component {
</div>
)
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
{this.renderUserForm()}
<Form>
{
this.getUserOrganization()?.accountItems?.map(accountItem => {
if (!accountItem.visible) {
return null;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (accountItem.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return null;
}
} else if (accountItem.viewRule === "Admin") {
if (!isAdmin) {
return null;
}
}
return (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
);
})
}
</Form>
</Card>
</div>
);

View File

@@ -38,7 +38,7 @@ class AccountTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self", tab: ""};
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self"};
if (table === undefined) {
table = [];
}
@@ -93,19 +93,6 @@ class AccountTable extends React.Component {
);
},
},
{
title: i18next.t("general:Tab"),
dataIndex: "tab",
key: "tab",
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} placeholder={i18next.t("user:Default")} onChange={e => {
this.updateField(table, index, "tab", e.target.value);
}} />
);
},
},
{
title: i18next.t("signup:Regex"),
dataIndex: "regex",

View File

@@ -275,7 +275,7 @@ export function getTransactionTableColumns(options = {}) {
title: i18next.t("transaction:Amount"),
dataIndex: "amount",
key: "amount",
width: "180px",
width: "160px",
sorter: getSorter("amount"),
...(getColumnSearchProps ? getColumnSearchProps("amount") : {}),
fixed: (Setting.isMobile()) ? "false" : "right",