@@ -80,6 +80,18 @@ func GetEmailAddressByID(uid, id int64) (*EmailAddress, error) { | |||
return email, nil | |||
} | |||
// GetEmailAddressByIDAndEmail gets a user's email address by ID and email | |||
func GetEmailAddressByIDAndEmail(uid int64, emailAddr string) (*EmailAddress, error) { | |||
// User ID is required for security reasons | |||
email := &EmailAddress{UID: uid, Email: emailAddr} | |||
if has, err := x.Get(email); err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, nil | |||
} | |||
return email, nil | |||
} | |||
func isEmailActive(e Engine, email string, userID, emailID int64) (bool, error) { | |||
if len(email) == 0 { | |||
return true, nil | |||
@@ -141,6 +141,35 @@ func DeleteAttachment(ctx *context.Context) { | |||
}) | |||
} | |||
func DownloadUserIsOrgOrCollaboration(ctx *context.Context, attach *models.Attachment) bool { | |||
dataset, err := models.GetDatasetByID(attach.DatasetID) | |||
if err != nil { | |||
log.Info("query dataset error") | |||
} else { | |||
repo, err := models.GetRepositoryByID(dataset.RepoID) | |||
if err != nil { | |||
log.Info("query repo error.") | |||
} else { | |||
repo.GetOwner() | |||
if ctx.User != nil { | |||
if repo.Owner.IsOrganization() { | |||
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) { | |||
log.Info("org user may visit the attach.") | |||
return true | |||
} | |||
} | |||
isCollaborator, _ := repo.IsCollaborator(ctx.User.ID) | |||
if isCollaborator { | |||
log.Info("Collaborator user may visit the attach.") | |||
return true | |||
} | |||
} | |||
} | |||
} | |||
return false | |||
} | |||
// GetAttachment serve attachements | |||
func GetAttachment(ctx *context.Context) { | |||
typeCloudBrain := ctx.QueryInt("type") | |||
@@ -165,16 +194,29 @@ func GetAttachment(ctx *context.Context) { | |||
ctx.ServerError("LinkedRepository", err) | |||
return | |||
} | |||
dataSet, err := attach.LinkedDataSet() | |||
if err != nil { | |||
ctx.ServerError("LinkedDataSet", err) | |||
return | |||
} | |||
if repository == nil && dataSet != nil { | |||
repository, _ = models.GetRepositoryByID(dataSet.RepoID) | |||
unitType = models.UnitTypeDatasets | |||
} | |||
if repository == nil { //If not linked | |||
if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && attach.IsPrivate { //We block if not the uploader | |||
//if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && attach.IsPrivate { //We block if not the uploader | |||
//log.Info("ctx.IsSigned =" + fmt.Sprintf("%v", ctx.IsSigned)) | |||
if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && attach.IsPrivate && !DownloadUserIsOrgOrCollaboration(ctx, attach) { //We block if not the uploader | |||
ctx.Error(http.StatusNotFound) | |||
return | |||
} | |||
} else { //If we have the repository we check access | |||
perm, err := models.GetUserRepoPermission(repository, ctx.User) | |||
if err != nil { | |||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) | |||
perm, errPermission := models.GetUserRepoPermission(repository, ctx.User) | |||
if errPermission != nil { | |||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", errPermission.Error()) | |||
return | |||
} | |||
if !perm.CanRead(unitType) { | |||
@@ -183,12 +225,6 @@ func GetAttachment(ctx *context.Context) { | |||
} | |||
} | |||
dataSet, err := attach.LinkedDataSet() | |||
if err != nil { | |||
ctx.ServerError("LinkedDataSet", err) | |||
return | |||
} | |||
if dataSet != nil { | |||
isPermit, err := models.GetUserDataSetPermission(dataSet, ctx.User) | |||
if err != nil { | |||
@@ -205,7 +241,7 @@ func GetAttachment(ctx *context.Context) { | |||
if setting.Attachment.StoreType == storage.MinioStorageType { | |||
url := "" | |||
if typeCloudBrain == models.TypeCloudBrainOne { | |||
url, err = storage.Attachments.PresignedGetURL(setting.Attachment.Minio.BasePath + attach.RelativePath(), attach.Name) | |||
url, err = storage.Attachments.PresignedGetURL(setting.Attachment.Minio.BasePath+attach.RelativePath(), attach.Name) | |||
if err != nil { | |||
ctx.ServerError("PresignedGetURL", err) | |||
return | |||
@@ -1,15 +1,13 @@ | |||
package repo | |||
import ( | |||
"sort" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/auth" | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"sort" | |||
) | |||
const ( | |||
@@ -24,19 +22,42 @@ func MustEnableDataset(ctx *context.Context) { | |||
} | |||
} | |||
func filterPrivateAttachments(ctx *context.Context, list []*models.Attachment) []*models.Attachment { | |||
func newFilterPrivateAttachments(ctx *context.Context, list []*models.Attachment, repo *models.Repository) []*models.Attachment { | |||
if ctx.Repo.CanWrite(models.UnitTypeDatasets) { | |||
log.Info("can write.") | |||
return list | |||
} else { | |||
if repo.Owner == nil { | |||
repo.GetOwner() | |||
} | |||
permission := false | |||
if repo.Owner.IsOrganization() && ctx.User != nil { | |||
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) { | |||
log.Info("user is member of org.") | |||
permission = true | |||
} | |||
} | |||
if !permission && ctx.User != nil { | |||
isCollaborator, _ := repo.IsCollaborator(ctx.User.ID) | |||
if isCollaborator { | |||
log.Info("Collaborator user may visit the attach.") | |||
permission = true | |||
} | |||
} | |||
var publicList []*models.Attachment | |||
for _, attach := range list { | |||
if !attach.IsPrivate { | |||
publicList = append(publicList, attach) | |||
} else { | |||
if permission { | |||
publicList = append(publicList, attach) | |||
} | |||
} | |||
} | |||
return publicList | |||
} | |||
} | |||
func DatasetIndex(ctx *context.Context) { | |||
@@ -60,7 +81,7 @@ func DatasetIndex(ctx *context.Context) { | |||
ctx.ServerError("GetDatasetAttachments", err) | |||
return | |||
} | |||
attachments := filterPrivateAttachments(ctx, dataset.Attachments) | |||
attachments := newFilterPrivateAttachments(ctx, dataset.Attachments, repo) | |||
ctx.Data["SortType"] = ctx.Query("sort") | |||
switch ctx.Query("sort") { | |||
@@ -569,10 +569,19 @@ func safeURL(address string) string { | |||
} | |||
type ContributorInfo struct { | |||
UserInfo *models.User | |||
Email string // for contributor who is not a registered user | |||
UserInfo *models.User // nil for contributor who is not a registered user | |||
Email string | |||
CommitCnt int | |||
} | |||
func getContributorInfo(contributorInfos []*ContributorInfo, email string) *ContributorInfo{ | |||
for _, c := range contributorInfos { | |||
if strings.Compare(c.Email,email) == 0 { | |||
return c | |||
} | |||
} | |||
return nil | |||
} | |||
// Home render repository home page | |||
func Home(ctx *context.Context) { | |||
if len(ctx.Repo.Units) > 0 { | |||
@@ -581,15 +590,31 @@ func Home(ctx *context.Context) { | |||
if err == nil && contributors != nil { | |||
var contributorInfos []*ContributorInfo | |||
for _, c := range contributors { | |||
// get user info from committer email | |||
user, err := models.GetUserByEmail(c.Email) | |||
if err == nil { | |||
contributorInfos = append(contributorInfos, &ContributorInfo{ | |||
user, c.Email, | |||
}) | |||
// committer is system user, get info through user's primary email | |||
existedContributorInfo := getContributorInfo(contributorInfos,user.Email) | |||
if existedContributorInfo != nil { | |||
// existed: same primary email, different committer name | |||
existedContributorInfo.CommitCnt += c.CommitCnt | |||
}else{ | |||
// new committer info | |||
contributorInfos = append(contributorInfos, &ContributorInfo{ | |||
user, user.Email,c.CommitCnt, | |||
}) | |||
} | |||
} else { | |||
contributorInfos = append(contributorInfos, &ContributorInfo{ | |||
nil, c.Email, | |||
}) | |||
// committer is not system user | |||
existedContributorInfo := getContributorInfo(contributorInfos,c.Email) | |||
if existedContributorInfo != nil { | |||
// existed: same primary email, different committer name | |||
existedContributorInfo.CommitCnt += c.CommitCnt | |||
}else{ | |||
contributorInfos = append(contributorInfos, &ContributorInfo{ | |||
nil, c.Email,c.CommitCnt, | |||
}) | |||
} | |||
} | |||
} | |||
ctx.Data["ContributorInfo"] = contributorInfos | |||
@@ -104,6 +104,21 @@ func CreateUser(ctx *context.Context, form api.CreateUserOption) { | |||
} | |||
return | |||
} | |||
err := models.AddEmailAddress(&models.EmailAddress{ | |||
UID: u.ID, | |||
Email: form.Email, | |||
IsActivated: !setting.Service.RegisterEmailConfirm, | |||
}) | |||
if err != nil { | |||
log.Error("AddEmailAddress failed:%v", err.Error(), ctx.Data["MsgID"]) | |||
ctx.JSON(http.StatusInternalServerError, map[string]string{ | |||
"error_msg": err.Error(), | |||
}) | |||
return | |||
} | |||
log.Trace("Account created (%s): %s", ctx.User.Name, u.Name, ctx.Data["MsgID"]) | |||
// Send email notification. | |||
@@ -1165,7 +1165,19 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo | |||
} | |||
return | |||
} | |||
log.Trace("Account created: %s", u.Name) | |||
log.Trace("Account created: %s", u.Name, ctx.Data["MsgID"]) | |||
err := models.AddEmailAddress(&models.EmailAddress{ | |||
UID: u.ID, | |||
Email: form.Email, | |||
IsActivated: !setting.Service.RegisterEmailConfirm, | |||
}) | |||
if err != nil { | |||
log.Error("AddEmailAddress failed:%v", err.Error(), ctx.Data["MsgID"]) | |||
ctx.ServerError("AddEmailAddress", err) | |||
return | |||
} | |||
// Auto-set admin for the only user. | |||
if models.CountUsers() == 1 { | |||
@@ -1254,6 +1266,15 @@ func Activate(ctx *context.Context) { | |||
log.Error("Error storing session: %v", err) | |||
} | |||
email, err := models.GetEmailAddressByIDAndEmail(user.ID, user.Email) | |||
if err != nil || email == nil{ | |||
log.Error("GetEmailAddressByIDAndEmail failed", ctx.Data["MsgID"]) | |||
} else { | |||
if err := email.Activate(); err != nil { | |||
log.Error("Activate failed: %v", err, ctx.Data["MsgID"]) | |||
} | |||
} | |||
ctx.Flash.Success(ctx.Tr("auth.account_activated")) | |||
ctx.Redirect(setting.AppSubURL + "/") | |||
return | |||
@@ -96,6 +96,18 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) { | |||
ctx.User.Location = form.Location | |||
ctx.User.Language = form.Language | |||
ctx.User.Description = form.Description | |||
isUsed, err := models.IsEmailUsed(form.Email) | |||
if err != nil { | |||
ctx.ServerError("IsEmailUsed", err) | |||
return | |||
} | |||
if isUsed { | |||
ctx.Flash.Error(ctx.Tr("form.email_been_used")) | |||
ctx.Redirect(setting.AppSubURL + "/user/settings") | |||
return | |||
} | |||
if err := models.UpdateUserSetting(ctx.User); err != nil { | |||
if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | |||
ctx.Flash.Error(ctx.Tr("form.email_been_used")) | |||
@@ -4,6 +4,48 @@ | |||
font-size: 1.0em; | |||
margin-bottom: 1.0rem; | |||
} | |||
#contributorInfo > a:nth-child(n+25){ | |||
display:none; | |||
} | |||
#contributorInfo > a{ | |||
width: 2.0em; | |||
float: left; | |||
margin: .25em; | |||
} | |||
#contributorInfo > a.circular{ | |||
height: 2.0em; | |||
padding: 0; | |||
overflow: hidden; | |||
letter-spacing:1.0em; | |||
text-indent: 0.6em; | |||
line-height: 2.0em; | |||
text-transform:capitalize; | |||
color: #FFF; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+1){ | |||
background-color: #4ccdec; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+2){ | |||
background-color: #e0b265; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+3){ | |||
background-color: #d884b7; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+4){ | |||
background-color: #8c6bdc; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+5){ | |||
background-color: #3cb99f; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+6){ | |||
background-color: #6995b9; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+7){ | |||
background-color: #ab91a7; | |||
} | |||
#contributorInfo > a.circular:nth-child(9n+8){ | |||
background-color: #bfd0aa; | |||
} | |||
</style> | |||
<div class="repository file list"> | |||
{{template "repo/header" .}} | |||
@@ -193,17 +235,15 @@ | |||
<h4 class="ui header"> | |||
<strong>贡献者 ({{len .ContributorInfo}})</strong> | |||
<div class="ui right"> | |||
<a class="text grey" href="">全部 {{svg "octicon-chevron-right" 16}}</a> | |||
<a class="membersmore text grey" href="javascript:;">全部 {{svg "octicon-chevron-right" 16}}</a> | |||
</div> | |||
</h4> | |||
<div class="ui members"> | |||
<div class="ui members" id="contributorInfo"> | |||
{{range .ContributorInfo}} | |||
{{/*<img class="ui avatar image" src="{{.UserInfo.RelAvatarLink}}" alt=""/> <a href="{{AppSubUrl}}/{{.UserInfo.Name}}">{{.UserInfo.Name}}</a>*/}} | |||
{{if .UserInfo}} | |||
<a href="{{AppSubUrl}}/{{.UserInfo.Name}}"><img class="ui avatar image" src="{{.UserInfo.RelAvatarLink}}" alt=""/></a> | |||
<a href="{{AppSubUrl}}/{{.UserInfo.Name}}"><img class="ui avatar image" src="{{.UserInfo.RelAvatarLink}}"></a> | |||
{{else if .Email}} | |||
<a href="mailto:{{.Email}}"><img class="ui avatar image" src="{{AvatarLink .Email}}" alt=""/></a> | |||
<a href="mailto:{{.Email}}" class="circular ui button">{{.Email}}</a> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
@@ -215,4 +255,12 @@ | |||
</div> | |||
</div> | |||
<script type="text/javascript"> | |||
$(document).ready(function(){ | |||
$(".membersmore").click(function(){ | |||
$("#contributorInfo > a:nth-child(n+25)").show(); | |||
}); | |||
}); | |||
</script> | |||
{{template "base/footer" .}} |
@@ -335,6 +335,7 @@ export default { | |||
async function uploadMinio(url, e) { | |||
const res = await axios.put(url, e.target.result); | |||
delete e.target.result | |||
etags[currentChunk] = res.headers.etag; | |||
} | |||
@@ -2685,7 +2685,7 @@ tbody.commit-list { | |||
width: 1127px; | |||
} | |||
th .message-wrapper { | |||
max-width: 680px; | |||
max-width: 510px; | |||
} | |||
} | |||