mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-24 14:24:47 +08:00
organization creation
This commit is contained in:
parent
87c2af6a18
commit
5764d43230
@ -15,7 +15,15 @@
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-btn v-if="alwaysEditing || isEdited" color="success" small class="flex-right" :loading="loading.save" :disabled="!isEdited" @click="save">
|
||||
<v-btn
|
||||
v-if="(alwaysEditing || isEdited) && !noSaveBtn"
|
||||
color="success"
|
||||
small
|
||||
class="flex-right"
|
||||
:loading="loading.save"
|
||||
:disabled="!isEdited"
|
||||
@click="save"
|
||||
>
|
||||
<v-icon left>mdi-check</v-icon>
|
||||
{{ $t('general.save') }}
|
||||
</v-btn>
|
||||
@ -105,7 +113,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator';
|
||||
import { PropType } from 'vue';
|
||||
import { Joinable, JoinableMember } from 'hangar-internal';
|
||||
import { JoinableMember } from 'hangar-internal';
|
||||
import { PaginatedResult, Role, User } from 'hangar-api';
|
||||
import { UserAvatar } from '~/components/users';
|
||||
|
||||
@ -124,8 +132,8 @@ interface EditableMember {
|
||||
components: { UserAvatar },
|
||||
})
|
||||
export default class MemberList extends Vue {
|
||||
@Prop({ type: Object as PropType<Joinable>, required: true })
|
||||
joinable!: Joinable;
|
||||
@Prop({ type: Array as PropType<JoinableMember[]>, default: () => [] })
|
||||
members!: JoinableMember[];
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
alwaysEditing!: boolean;
|
||||
@ -133,6 +141,12 @@ export default class MemberList extends Vue {
|
||||
@Prop({ type: Array as PropType<Role[]>, required: true })
|
||||
roles!: Role[];
|
||||
|
||||
@Prop({ type: Function as PropType<(name: User) => boolean>, default: () => (_: User) => true })
|
||||
searchFilter!: (user: User) => boolean;
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
noSaveBtn!: boolean;
|
||||
|
||||
editing: boolean = false;
|
||||
editingMembers: EditableMember[] = [];
|
||||
userSearch: string = '';
|
||||
@ -153,7 +167,7 @@ export default class MemberList extends Vue {
|
||||
|
||||
setupEditing() {
|
||||
this.editingMembers = [];
|
||||
this.editingMembers = this.convertMembers(this.joinable.members);
|
||||
this.editingMembers = this.convertMembers(this.members);
|
||||
}
|
||||
|
||||
convertMembers(jms: JoinableMember[]): EditableMember[] {
|
||||
@ -174,12 +188,12 @@ export default class MemberList extends Vue {
|
||||
(em) =>
|
||||
em.toDelete ||
|
||||
(em.new && em.roleId) ||
|
||||
(em.editing && !em.new && em.roleId !== this.joinable.members.find((jm) => jm.user.name === em.name)!.role.role.roleId)
|
||||
(em.editing && !em.new && em.roleId !== this.members.find((jm) => jm.user.name === em.name)!.role.role.roleId)
|
||||
);
|
||||
}
|
||||
|
||||
stopEditing(member: EditableMember) {
|
||||
const originalMember = this.joinable.members.find((jm) => jm.user.name === member.name)!;
|
||||
const originalMember = this.members.find((jm) => jm.user.name === member.name)!;
|
||||
member.roleTitle = originalMember.role.role.title;
|
||||
member.roleId = originalMember.role.role.roleId;
|
||||
member.editing = false;
|
||||
@ -240,7 +254,7 @@ export default class MemberList extends Vue {
|
||||
offset: 0,
|
||||
})
|
||||
.then((users) => {
|
||||
this.users = users.result.filter((u) => this.editingMembers.findIndex((em) => em.name === u.name) === -1);
|
||||
this.users = users.result.filter(this.searchFilter).filter((u) => this.editingMembers.findIndex((em) => em.name === u.name) === -1);
|
||||
})
|
||||
.catch(this.$util.handleRequestError)
|
||||
.finally(() => {
|
||||
|
@ -380,6 +380,15 @@ const msgs: LocaleMessageObject = {
|
||||
title: 'Create a new Organization',
|
||||
text: 'Organizations allow you group users provide closer collaboration between them within your projects on Hangar.',
|
||||
name: 'Organization Name',
|
||||
error: {
|
||||
duplicateName: 'An organization/user with that name already exists',
|
||||
invalidName: 'Invalid organization name',
|
||||
tooManyOrgs: 'You can only create a maximum of {0} organizations',
|
||||
notEnabled: 'Organizations are not enabled!',
|
||||
jsonError: 'Error parsing the JSON response from HangarAuth',
|
||||
hangarAuthValidationError: 'Validation Error: {0}',
|
||||
unknownError: 'Unknown error while creating organization',
|
||||
},
|
||||
},
|
||||
},
|
||||
form: {
|
||||
@ -426,6 +435,9 @@ const msgs: LocaleMessageObject = {
|
||||
invite: 'You have been invited to join the group {0} on the project {1}',
|
||||
newVersion: 'A new version has been released for {0}: {1}',
|
||||
},
|
||||
organization: {
|
||||
invite: 'You have been invited to join the group {0} in the organization {1}',
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
notice: {
|
||||
|
@ -27,7 +27,7 @@
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else>
|
||||
<div v-else class="red--text text--lighten-2">
|
||||
{{ $t(`notifications.empty.${filters.notification}`) }}
|
||||
</div>
|
||||
</v-col>
|
||||
@ -75,7 +75,7 @@
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else>
|
||||
<div v-else class="red--text text--lighten-2">
|
||||
{{ $t('notifications.empty.invites') }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
@ -2,25 +2,121 @@
|
||||
<v-col cols="12" md="8" offset-md="2">
|
||||
<v-card>
|
||||
<v-card-title v-text="$t('organization.new.title')" />
|
||||
<v-card-subtitle>{{ $t('organization.new.text') }}</v-card-subtitle>
|
||||
<!--TODO error message if already at max orgs-->
|
||||
<v-card-text>
|
||||
{{ $t('organization.new.text') }}
|
||||
<v-text-field type="text" :label="$t('organization.new.name')"></v-text-field>
|
||||
<v-divider />
|
||||
<!--<UserSelectionForm />-->
|
||||
<v-form v-model="validForm">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
class="mt-2"
|
||||
filled
|
||||
:loading="validateLoading"
|
||||
:label="$t('organization.new.name')"
|
||||
:rules="[
|
||||
$util.$vc.require($t('organization.new.name')),
|
||||
$util.$vc.regex($t('organization.new.name'), validations.org.regex),
|
||||
$util.$vc.minLength(validations.org.min),
|
||||
$util.$vc.maxLength(validations.org.max),
|
||||
]"
|
||||
:error-messages="nameErrorMessages"
|
||||
/>
|
||||
<v-divider />
|
||||
<MemberList ref="memberList" class="mt-7 elevation-5" no-save-btn :roles="roles" always-editing :search-filter="searchFilter" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn>{{ $t('form.memberList.create') }}</v-btn>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn color="success" :disabled="!canCreate" :loading="loading" @click="create">
|
||||
<v-icon left>mdi-check</v-icon>
|
||||
{{ $t('form.memberList.create') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'nuxt-property-decorator';
|
||||
import { Component, Watch } from 'nuxt-property-decorator';
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
import { Context } from '@nuxt/types';
|
||||
import { Role, User } from 'hangar-api';
|
||||
import { HangarForm } from '~/components/mixins';
|
||||
import MemberList from '~/components/projects/MemberList.vue';
|
||||
|
||||
// TODO implement OrganizationsNewPage
|
||||
@Component
|
||||
export default class OrganizationsNewPage extends Vue {}
|
||||
@Component({
|
||||
components: { MemberList },
|
||||
})
|
||||
export default class OrganizationsNewPage extends HangarForm {
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('organization.new.title'),
|
||||
};
|
||||
}
|
||||
|
||||
roles!: Role[];
|
||||
nameErrorMessages: TranslateResult[] = [];
|
||||
validateLoading = false;
|
||||
form = {
|
||||
name: '',
|
||||
};
|
||||
|
||||
$refs!: {
|
||||
memberList: MemberList;
|
||||
};
|
||||
|
||||
searchFilter(user: User) {
|
||||
return user.name !== this.currentUser.name;
|
||||
}
|
||||
|
||||
get canCreate() {
|
||||
return (
|
||||
this.validForm &&
|
||||
!this.validateLoading &&
|
||||
(this.$refs.memberList.isEdited || (this.$refs.memberList.editedMembers.length === 0 && this.$refs.memberList.editingMembers.length === 0))
|
||||
);
|
||||
}
|
||||
|
||||
create() {
|
||||
this.loading = true;
|
||||
this.$api
|
||||
.requestInternal('organizations/create', true, 'post', {
|
||||
name: this.form.name,
|
||||
members: this.$refs.memberList.editedMembers,
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({
|
||||
name: 'user',
|
||||
params: {
|
||||
user: this.form.name,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(this.$util.handleRequestError)
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@Watch('form.name')
|
||||
checkName(val: string) {
|
||||
if (!val) return true;
|
||||
this.validateLoading = true;
|
||||
this.nameErrorMessages = [];
|
||||
this.$api
|
||||
.requestInternal('organizations/validate', false, 'get', { name: val })
|
||||
.catch(() => {
|
||||
this.nameErrorMessages.push(this.$t('organization.new.error.duplicateName'));
|
||||
})
|
||||
.finally(() => {
|
||||
this.validateLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
async asyncData({ $api, $util }: Context) {
|
||||
const orgRoles = await $api.requestInternal('data/orgRoles', false).catch($util.handlePageRequestError);
|
||||
return { roles: orgRoles };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -25,6 +25,7 @@ export interface RootState {
|
||||
maxPageCount: number;
|
||||
maxChannelCount: number;
|
||||
};
|
||||
org: Validation;
|
||||
userTagline: Validation;
|
||||
version: Validation;
|
||||
maxOrgCount: number;
|
||||
|
@ -3,12 +3,19 @@ package io.papermc.hangar.config.hangar;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.orgs")
|
||||
public class OrganizationsConfig {
|
||||
private boolean enabled = true;
|
||||
private String dummyEmailDomain = "org.papermc.io";
|
||||
private int createLimit = 5;
|
||||
private int minNameLen = 3;
|
||||
private int maxNameLen = 24;
|
||||
private String nameRegex = "[a-zA-Z0-9-_]*";
|
||||
private final Predicate<String> namePredicate = Pattern.compile(nameRegex).asMatchPredicate();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@ -33,4 +40,32 @@ public class OrganizationsConfig {
|
||||
public void setCreateLimit(int createLimit) {
|
||||
this.createLimit = createLimit;
|
||||
}
|
||||
|
||||
public int getMinNameLen() {
|
||||
return minNameLen;
|
||||
}
|
||||
|
||||
public void setMinNameLen(int minNameLen) {
|
||||
this.minNameLen = minNameLen;
|
||||
}
|
||||
|
||||
public String getNameRegex() {
|
||||
return nameRegex;
|
||||
}
|
||||
|
||||
public void setNameRegex(String nameRegex) {
|
||||
this.nameRegex = nameRegex;
|
||||
}
|
||||
|
||||
public int getMaxNameLen() {
|
||||
return maxNameLen;
|
||||
}
|
||||
|
||||
public void setMaxNameLen(int maxNameLen) {
|
||||
this.maxNameLen = maxNameLen;
|
||||
}
|
||||
|
||||
public boolean testName(String name) {
|
||||
return namePredicate.test(name);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@ -55,8 +56,8 @@ public class SSOSyncController {
|
||||
@ApiResponse(code = 200, message = "Ok"),
|
||||
@ApiResponse(code = 401, message = "Sent if the signature or API key missing or invalid.")
|
||||
})
|
||||
@PostMapping(value = "/sync_sso")
|
||||
public ResponseEntity<MultiValueMap<String, String>> syncSso(@RequestParam String sso, @RequestParam String sig, @RequestParam String apiKey) {
|
||||
@PostMapping(value = "/sync_sso", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public ResponseEntity<MultiValueMap<String, String>> syncSso(@RequestParam @NotEmpty String sso, @RequestParam @NotEmpty String sig, @RequestParam("api_key") @NotEmpty String apiKey) {
|
||||
if (!apiKey.equals(ssoConfig.getApiKey())) {
|
||||
log.warn("SSO sync failed: bad API key (" + apiKey + " provided, " + ssoConfig.getApiKey() + " expected)");
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "SSO sync failed: bad API key (" + apiKey + " provided, " + ssoConfig.getApiKey() + " expected)");
|
||||
|
@ -15,6 +15,7 @@ import io.papermc.hangar.model.common.NamedPermission;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import io.papermc.hangar.model.common.projects.Category;
|
||||
import io.papermc.hangar.model.common.projects.FlagReason;
|
||||
import io.papermc.hangar.model.common.roles.OrganizationRole;
|
||||
import io.papermc.hangar.model.common.roles.ProjectRole;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.service.internal.projects.PlatformService;
|
||||
@ -130,6 +131,11 @@ public class BackendDataController {
|
||||
return ResponseEntity.ok(ProjectRole.getAssignableRoles());
|
||||
}
|
||||
|
||||
@GetMapping("/orgRoles")
|
||||
public ResponseEntity<List<OrganizationRole>> getAssignableOrganizationRoles() {
|
||||
return ResponseEntity.ok(OrganizationRole.getAssignableRoles());
|
||||
}
|
||||
|
||||
@GetMapping("/validations")
|
||||
public ResponseEntity<ObjectNode> getValidations() {
|
||||
ObjectNode validations = mapper.createObjectNode();
|
||||
@ -145,6 +151,7 @@ public class BackendDataController {
|
||||
validations.set("project", projectValidations);
|
||||
validations.set("userTagline", mapper.valueToTree(new Validation(null, config.user.getMaxTaglineLen(), null)));
|
||||
validations.set("version", mapper.valueToTree(new Validation(config.projects.getVersionNameRegex(), null, null)));
|
||||
validations.set("org", mapper.valueToTree(new Validation(config.org.getNameRegex(), config.org.getMaxNameLen(), config.org.getMinNameLen())));
|
||||
validations.put("maxOrgCount", config.org.getCreateLimit());
|
||||
validations.put("urlRegex", config.getUrlRegex());
|
||||
return ResponseEntity.ok(validations);
|
||||
|
@ -0,0 +1,51 @@
|
||||
package io.papermc.hangar.controller.internal;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.internal.api.requests.CreateOrganizationForm;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
|
||||
import io.papermc.hangar.service.internal.organizations.OrganizationFactory;
|
||||
import io.papermc.hangar.service.internal.users.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/api/internal/organizations")
|
||||
public class OrganizationController {
|
||||
|
||||
private final UserService userService;
|
||||
private final OrganizationFactory organizationFactory;
|
||||
|
||||
@Autowired
|
||||
public OrganizationController(UserService userService, OrganizationFactory organizationFactory) {
|
||||
this.userService = userService;
|
||||
this.organizationFactory = organizationFactory;
|
||||
}
|
||||
|
||||
@Anyone
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@GetMapping("/validate")
|
||||
public void validateName(@RequestParam String name) {
|
||||
if (userService.getUserTable(name) != null) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PostMapping(path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void create(@Valid @RequestBody CreateOrganizationForm createOrganizationForm) {
|
||||
System.out.println(createOrganizationForm);
|
||||
organizationFactory.createOrganization(createOrganizationForm.getName(), createOrganizationForm.getNewMembers());
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ import io.papermc.hangar.security.annotations.permission.PermissionRequired;
|
||||
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
|
||||
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
|
||||
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
|
||||
import io.papermc.hangar.service.internal.OrganizationService;
|
||||
import io.papermc.hangar.service.internal.organizations.OrganizationService;
|
||||
import io.papermc.hangar.service.internal.projects.ProjectFactory;
|
||||
import io.papermc.hangar.service.internal.projects.ProjectService;
|
||||
import io.papermc.hangar.service.internal.users.UserService;
|
||||
|
@ -43,6 +43,13 @@ public class Validations {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean min(String value, int min) {
|
||||
if (value != null) {
|
||||
return value.length() >= min;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isEmpty(String value) {
|
||||
return value == null || value.isBlank() || value.isEmpty();
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@Controller("oldOrganizationController")
|
||||
public class OrganizationController extends HangarController {
|
||||
|
||||
private static final String STATUS_DECLINE = "decline";
|
||||
|
@ -36,4 +36,7 @@ public interface OrganizationDAO {
|
||||
" WHERE ot.user_id = :userId" +
|
||||
" AND (ot.permission & :permission::bit(64)) = ot.permission")
|
||||
List<OrganizationTable> getOrganizationsWithPermission(long userId, Permission permission);
|
||||
|
||||
@SqlQuery("SELECT * FROM organizations WHERE owner_id = :ownerId")
|
||||
List<OrganizationTable> getOrganizationsOwnedBy(long ownerId);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package io.papermc.hangar.db.dao.internal.table.members;
|
||||
|
||||
import io.papermc.hangar.model.db.members.OrganizationMemberTable;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
@ -14,7 +15,7 @@ public interface OrganizationMembersDAO extends MembersDAO<OrganizationMemberTab
|
||||
@Override
|
||||
@GetGeneratedKeys
|
||||
@SqlUpdate("INSERT INTO organization_members (user_id, organization_id) VALUES (:userId, :organizationId)")
|
||||
OrganizationMemberTable insert(OrganizationMemberTable table);
|
||||
OrganizationMemberTable insert(@BindBean OrganizationMemberTable table);
|
||||
|
||||
@Override
|
||||
@SqlQuery("SELECT * FROM organization_members WHERE organization_id = :organizationId AND user_id = :userId")
|
||||
|
@ -1,12 +1,19 @@
|
||||
package io.papermc.hangar.db.dao.internal.table.roles;
|
||||
|
||||
import io.papermc.hangar.db.mappers.RoleMapperFactory;
|
||||
import io.papermc.hangar.model.db.roles.OrganizationRoleTable;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterColumnMapperFactory;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.customizer.Timestamped;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RegisterConstructorMapper(OrganizationRoleTable.class)
|
||||
@RegisterColumnMapperFactory(RoleMapperFactory.class)
|
||||
public interface OrganizationRolesDAO extends IRolesDAO<OrganizationRoleTable> {
|
||||
|
||||
@Override
|
||||
|
@ -33,7 +33,9 @@ public enum GlobalRole implements Role<GlobalRoleTable> {
|
||||
QUARTZ_DONOR("Quartz_Donor",15, Permission.None, "Quartz Donor", Color.QUARTZ, 4),
|
||||
IRON_DONOR("Iron_Donor",16, Permission.None, "Iron Donor", Color.SILVER, 3),
|
||||
GOLD_DONOR("Gold_Donor",17, Permission.None, "Gold Donor", Color.GOLD, 2),
|
||||
DIAMOND_DONOR("Diamond_Donor",18, Permission.None, "Diamond Donor", Color.LIGHTBLUE, 1);
|
||||
DIAMOND_DONOR("Diamond_Donor",18, Permission.None, "Diamond Donor", Color.LIGHTBLUE, 1),
|
||||
|
||||
ORGANIZATION("Organization", 23, OrganizationRole.ORGANIZATION_OWNER.getPermissions(), "Organization", Color.PURPLE);
|
||||
|
||||
private final String value;
|
||||
private final long roleId;
|
||||
@ -103,7 +105,7 @@ public enum GlobalRole implements Role<GlobalRoleTable> {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GlobalRoleTable create(@Nullable Long principalId, long userId, boolean isAccepted) {
|
||||
public GlobalRoleTable create(@Nullable Long __ignoreThis, long userId, boolean __ignoreThisToo) {
|
||||
return new GlobalRoleTable(userId, this);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.postgresql.shaded.com.ongres.scram.common.util.Preconditions;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
|
||||
public enum OrganizationRole implements Role<OrganizationRoleTable> {
|
||||
|
||||
@ -17,8 +21,7 @@ public enum OrganizationRole implements Role<OrganizationRoleTable> {
|
||||
ORGANIZATION_EDITOR("Organization_Editor", 27, ProjectRole.PROJECT_EDITOR.getPermissions().add(ORGANIZATION_SUPPORT.permissions), "Editor", Color.TRANSPARENT),
|
||||
ORGANIZATION_DEVELOPER("Organization_Developer", 26, Permission.CreateProject.add(Permission.EditProjectSettings).add(ProjectRole.PROJECT_DEVELOPER.getPermissions()).add(ORGANIZATION_EDITOR.permissions), "Developer", Color.TRANSPARENT),
|
||||
ORGANIZATION_ADMIN("Organization_Admin", 25, Permission.EditApiKeys.add(Permission.ManageProjectMembers).add(Permission.EditOwnUserSettings).add(Permission.DeleteProject).add(Permission.DeleteVersion).add(ORGANIZATION_DEVELOPER.permissions), "Admin", Color.TRANSPARENT),
|
||||
ORGANIZATION_OWNER("Organization_Owner", 24, Permission.IsOrganizationOwner.add(ProjectRole.PROJECT_OWNER.getPermissions()).add(ORGANIZATION_ADMIN.permissions), "Owner", Color.PURPLE, false),
|
||||
ORGANIZATION("Organization", 23, ORGANIZATION_OWNER.permissions, "Organization", Color.PURPLE, false);
|
||||
ORGANIZATION_OWNER("Organization_Owner", 24, Permission.IsOrganizationOwner.add(ProjectRole.PROJECT_OWNER.getPermissions()).add(ORGANIZATION_ADMIN.permissions), "Owner", Color.PURPLE, false);
|
||||
|
||||
private final String value;
|
||||
private final long roleId;
|
||||
@ -28,8 +31,10 @@ public enum OrganizationRole implements Role<OrganizationRoleTable> {
|
||||
private final boolean isAssignable;
|
||||
|
||||
private final static OrganizationRole[] VALUES = values();
|
||||
private final static List<OrganizationRole> ASSIGNABLE_ROLES = Arrays.stream(VALUES).filter(OrganizationRole::isAssignable).collect(Collectors.toList());
|
||||
|
||||
public static OrganizationRole[] getValues() { return VALUES; }
|
||||
public static List<OrganizationRole> getAssignableRoles() { return ASSIGNABLE_ROLES; }
|
||||
|
||||
OrganizationRole(String value, long roleId, Permission permissions, String title, Color color) {
|
||||
this(value, roleId, permissions, title, color, true);
|
||||
@ -93,8 +98,8 @@ public enum OrganizationRole implements Role<OrganizationRoleTable> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OrganizationRoleTable create(@Nullable Long principalId, long userId, boolean isAccepted) {
|
||||
Preconditions.checkNotNull(principalId, "organization id");
|
||||
return new OrganizationRoleTable(userId, this, isAccepted, principalId);
|
||||
public @NotNull OrganizationRoleTable create(Long organizationId, long userId, boolean isAccepted) {
|
||||
Preconditions.checkNotNull(organizationId, "organization id");
|
||||
return new OrganizationRoleTable(userId, this, isAccepted, organizationId);
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +104,8 @@ public enum ProjectRole implements Role<ProjectRoleTable> {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ProjectRoleTable create(@Nullable Long principalId, long userId, boolean isAccepted) {
|
||||
Preconditions.checkNotNull(principalId, "project id");
|
||||
return new ProjectRoleTable(userId, this, isAccepted, principalId);
|
||||
public ProjectRoleTable create(Long projectId, long userId, boolean isAccepted) {
|
||||
Preconditions.checkNotNull(projectId, "project id");
|
||||
return new ProjectRoleTable(userId, this, isAccepted, projectId);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package io.papermc.hangar.model.db;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.papermc.hangar.model.db.projects.ProjectOwner;
|
||||
import io.papermc.hangar.model.internal.sso.AuthUser;
|
||||
import org.jdbi.v3.core.mapper.PropagateNull;
|
||||
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -43,6 +44,15 @@ public class UserTable extends Table implements ProjectOwner {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
// For use when creating orgs (when fake user is enabled)
|
||||
public UserTable(AuthUser authUser) {
|
||||
super(authUser.getId());
|
||||
this.name = authUser.getUserName();
|
||||
this.email = authUser.getEmail();
|
||||
this.readPrompts = List.of();
|
||||
this.language = authUser.getLang().toLanguageTag();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
|
@ -0,0 +1,30 @@
|
||||
package io.papermc.hangar.model.internal.api.requests;
|
||||
|
||||
import io.papermc.hangar.controller.validations.Validate;
|
||||
import io.papermc.hangar.model.common.roles.OrganizationRole;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CreateOrganizationForm extends EditMembersForm<OrganizationRole> {
|
||||
|
||||
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.org.nameRegex)", message = "organizations.new.error.invalidName")
|
||||
@Validate(SpEL = "@validate.max(#root, @hangarConfig.org.maxNameLen)", message = "organizations.new.error.invalidName")
|
||||
@Validate(SpEL = "@validate.min(#root, @hangarConfig.org.minNameLen)", message = "organizations.new.error.invalidName")
|
||||
private final String name;
|
||||
|
||||
public CreateOrganizationForm(List<Member<OrganizationRole>> members, String name) {
|
||||
super(members);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CreateOrganizationForm{" +
|
||||
"name='" + name + '\'' +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
@ -33,6 +33,15 @@ public class AuthUser {
|
||||
}
|
||||
}
|
||||
|
||||
public AuthUser(String userName, String email) {
|
||||
this.id = -100;
|
||||
this.userName = userName;
|
||||
this.email = email;
|
||||
this.avatarUrl = "";
|
||||
this.lang = Locale.ENGLISH;
|
||||
this.globalRoles = new ArrayList<>();
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
@ -14,6 +14,5 @@ import java.lang.annotation.Target;
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasAnyRole('ANONYMOUS', 'USER')")
|
||||
// TODO need a better name for this annotation
|
||||
public @interface Anyone {
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
package io.papermc.hangar.security.annotations;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
public @interface LoggedIn {
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package io.papermc.hangar.service.internal.organizations;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.OrganizationContext;
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.OrganizationDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.UserDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.exceptions.MultiHangarApiException;
|
||||
import io.papermc.hangar.model.common.roles.GlobalRole;
|
||||
import io.papermc.hangar.model.common.roles.OrganizationRole;
|
||||
import io.papermc.hangar.model.db.OrganizationTable;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
|
||||
import io.papermc.hangar.model.internal.sso.AuthUser;
|
||||
import io.papermc.hangar.service.HangarService;
|
||||
import io.papermc.hangar.service.internal.roles.GlobalRoleService;
|
||||
import io.papermc.hangar.service.internal.roles.MemberService.OrganizationMemberService;
|
||||
import io.papermc.hangar.service.internal.users.NotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException.UnprocessableEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class OrganizationFactory extends HangarService {
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final OrganizationDAO organizationDAO;
|
||||
private final OrganizationService organizationService;
|
||||
private final OrganizationMemberService organizationMemberService;
|
||||
private final GlobalRoleService globalRoleService;
|
||||
private final NotificationService notificationService;
|
||||
private final ObjectMapper mapper;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
public OrganizationFactory(HangarDao<UserDAO> userDAO, HangarDao<OrganizationDAO> organizationDAO, OrganizationService organizationService, OrganizationMemberService organizationMemberService, GlobalRoleService globalRoleService, NotificationService notificationService, ObjectMapper mapper, RestTemplate restTemplate) {
|
||||
this.userDAO = userDAO.get();
|
||||
this.organizationDAO = organizationDAO.get();
|
||||
this.organizationService = organizationService;
|
||||
this.organizationMemberService = organizationMemberService;
|
||||
this.globalRoleService = globalRoleService;
|
||||
this.notificationService = notificationService;
|
||||
this.mapper = mapper;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
public void createOrganization(String name, List<Member<OrganizationRole>> members) {
|
||||
if (!config.org.isEnabled()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.notEnabled");
|
||||
}
|
||||
if (organizationService.getOrganizationsOwnedBy(getHangarPrincipal().getId()).size() >= config.org.getCreateLimit()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.tooManyOrgs", config.org.getCreateLimit());
|
||||
}
|
||||
|
||||
String dummyEmail = name.replaceAll("[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]", "") + '@' + config.org.getDummyEmailDomain();
|
||||
AuthUser authOrganizationUser;
|
||||
if (!config.fakeUser.isEnabled()) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
|
||||
map.add("api-key", config.sso.getApiKey());
|
||||
map.add("username", name);
|
||||
map.add("email", dummyEmail);
|
||||
map.add("verified", Boolean.TRUE.toString());
|
||||
map.add("dummy", Boolean.TRUE.toString());
|
||||
try {
|
||||
authOrganizationUser = mapper.treeToValue(restTemplate.postForObject(config.security.api.getUrl() + "/api/users", new HttpEntity<>(map, headers), ObjectNode.class), AuthUser.class);
|
||||
} catch (UnprocessableEntity e) {
|
||||
try {
|
||||
ObjectNode objectNode = mapper.readValue(e.getResponseBodyAsByteArray(), ObjectNode.class);
|
||||
List<HangarApiException> errors = new ArrayList<>();
|
||||
for (JsonNode jsonNode : objectNode.get("error")) {
|
||||
errors.add(new HangarApiException("organization.new.error.hangarAuthValidationError", jsonNode.asText()));
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw new MultiHangarApiException(errors);
|
||||
}
|
||||
} catch (IOException __) {
|
||||
throw new HangarApiException(HttpStatus.INTERNAL_SERVER_ERROR, "organization.new.error.unknownError");
|
||||
}
|
||||
|
||||
throw new HangarApiException(HttpStatus.INTERNAL_SERVER_ERROR, "organization.new.error.unknownError");
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new HangarApiException(HttpStatus.INTERNAL_SERVER_ERROR, "organization.new.error.jsonError");
|
||||
}
|
||||
} else {
|
||||
authOrganizationUser = new AuthUser(name, dummyEmail);
|
||||
userDAO.insert(new UserTable(authOrganizationUser));
|
||||
}
|
||||
|
||||
// Just a note, the /api/sync_sso creates the org user here, so it will already be created when the above response is returned
|
||||
UserTable userTable = userDAO.getUserTable(authOrganizationUser.getId());
|
||||
OrganizationTable organizationTable = organizationDAO.insert(new OrganizationTable(authOrganizationUser.getId(), name, getHangarPrincipal().getId(), userTable.getId()));
|
||||
globalRoleService.addRole(GlobalRole.ORGANIZATION.create(null, userTable.getId(), false));
|
||||
organizationMemberService.addMember(organizationTable.getId(), OrganizationRole.ORGANIZATION_OWNER.create(organizationTable.getId(), getHangarPrincipal().getId(), true));
|
||||
|
||||
List<String> newLogState = new ArrayList<>();
|
||||
members.forEach(member -> {
|
||||
UserTable memberTable = userDAO.getUserTable(member.getName());
|
||||
if (memberTable == null) {
|
||||
// TODO errors
|
||||
return;
|
||||
}
|
||||
newLogState.add(member.getName() + ": " + member.getRole().getTitle());
|
||||
organizationMemberService.addMember(organizationTable.getId(), member.getRole().create(organizationTable.getId(), memberTable.getId(), false));
|
||||
notificationService.notifyNewOrganizationMember(member, memberTable.getId(), organizationTable);
|
||||
});
|
||||
userActionLogService.organization(LoggedActionType.ORG_MEMBERS_ADDED.with(OrganizationContext.of(organizationTable.getId())), String.join("<br>", newLogState), "<i>No Members</i>");
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package io.papermc.hangar.service.internal;
|
||||
package io.papermc.hangar.service.internal.organizations;
|
||||
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.OrganizationDAO;
|
||||
@ -27,4 +27,8 @@ public class OrganizationService extends HangarService {
|
||||
public List<OrganizationTable> getOrganizationTablesWithPermission(long userId, Permission permission) {
|
||||
return organizationDAO.getOrganizationsWithPermission(userId, permission);
|
||||
}
|
||||
|
||||
public List<OrganizationTable> getOrganizationsOwnedBy(long ownerId) {
|
||||
return organizationDAO.getOrganizationsOwnedBy(ownerId);
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ import io.papermc.hangar.model.internal.user.JoinableMember;
|
||||
import io.papermc.hangar.service.HangarService;
|
||||
import io.papermc.hangar.service.PermissionService;
|
||||
import io.papermc.hangar.service.VisibilityService.ProjectVisibilityService;
|
||||
import io.papermc.hangar.service.internal.OrganizationService;
|
||||
import io.papermc.hangar.service.internal.organizations.OrganizationService;
|
||||
import io.papermc.hangar.service.internal.roles.MemberService.ProjectMemberService;
|
||||
import io.papermc.hangar.service.internal.roles.RoleService.ProjectRoleService;
|
||||
import io.papermc.hangar.service.internal.uploads.ProjectFiles;
|
||||
|
@ -5,8 +5,10 @@ import io.papermc.hangar.db.dao.internal.HangarNotificationsDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.NotificationsDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.common.roles.OrganizationRole;
|
||||
import io.papermc.hangar.model.common.roles.ProjectRole;
|
||||
import io.papermc.hangar.model.db.NotificationTable;
|
||||
import io.papermc.hangar.model.db.OrganizationTable;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.db.projects.ProjectTable;
|
||||
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
|
||||
@ -77,4 +79,8 @@ public class NotificationService extends HangarService {
|
||||
public void notifyNewProjectMember(Member<ProjectRole> member, long userId, ProjectTable projectTable) {
|
||||
notificationsDAO.insert(new NotificationTable(userId, NotificationType.PROJECT_INVITE, null, projectTable.getId(), new String[]{"notifications.project.invite", member.getRole().getTitle(), projectTable.getName()}));
|
||||
}
|
||||
|
||||
public void notifyNewOrganizationMember(Member<OrganizationRole> member, long userId, OrganizationTable organizationTable) {
|
||||
notificationsDAO.insert(new NotificationTable(userId, NotificationType.ORGANIZATION_INVITE, null, organizationTable.getId(), new String[]{"notifications.organization.invite", member.getRole().getTitle(), organizationTable.getName()}));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user