Files
terraform-provider-unifi/internal/provider/settings/resource_setting_guest_access.go
Mateusz Filipowicz a133383b43 feat: support customizing guest portal logo and background with unifi_portal_file and unifi_setting_guest_access resources (#74)
* feat: support customizing guest portal logo and background with `unifi_portal_file` and `unifi_setting_guest_access` resources

* ci: run acceptance tests on go.mod changes

* f
2025-03-22 17:31:30 +01:00

1604 lines
60 KiB
Go

package settings
import (
"context"
"fmt"
ut "github.com/filipowm/terraform-provider-unifi/internal/provider/types"
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
)
// TODO add support for uploading files and configuring logo and background custom images
type guestAccessModel struct {
base.Model
AllowedSubnet types.String `tfsdk:"allowed_subnet"`
RestrictedSubnet types.String `tfsdk:"restricted_subnet"`
Auth types.String `tfsdk:"auth"`
AuthUrl types.String `tfsdk:"auth_url"`
Authorize types.Object `tfsdk:"authorize"`
CustomIP types.String `tfsdk:"custom_ip"`
EcEnabled types.Bool `tfsdk:"ec_enabled"`
Expire types.Int32 `tfsdk:"expire"`
ExpireNumber types.Int32 `tfsdk:"expire_number"`
ExpireUnit types.Int32 `tfsdk:"expire_unit"`
FacebookEnabled types.Bool `tfsdk:"facebook_enabled"`
Facebook types.Object `tfsdk:"facebook"`
FacebookWifi types.Object `tfsdk:"facebook_wifi"`
GoogleEnabled types.Bool `tfsdk:"google_enabled"`
Google types.Object `tfsdk:"google"`
IPpay types.Object `tfsdk:"ippay"`
MerchantWarrior types.Object `tfsdk:"merchant_warrior"`
PasswordEnabled types.Bool `tfsdk:"password_enabled"`
Password types.String `tfsdk:"password"`
PaymentEnabled types.Bool `tfsdk:"payment_enabled"`
PaymentGateway types.String `tfsdk:"payment_gateway"`
Paypal types.Object `tfsdk:"paypal"`
PortalCustomization types.Object `tfsdk:"portal_customization"`
PortalEnabled types.Bool `tfsdk:"portal_enabled"`
PortalHostname types.String `tfsdk:"portal_hostname"`
PortalUseHostname types.Bool `tfsdk:"portal_use_hostname"`
Quickpay types.Object `tfsdk:"quickpay"`
RadiusEnabled types.Bool `tfsdk:"radius_enabled"`
Radius types.Object `tfsdk:"radius"`
RedirectEnabled types.Bool `tfsdk:"redirect_enabled"`
Redirect types.Object `tfsdk:"redirect"`
RestrictedDNSEnabled types.Bool `tfsdk:"restricted_dns_enabled"`
RestrictedDNSServers types.List `tfsdk:"restricted_dns_servers"`
TemplateEngine types.String `tfsdk:"template_engine"`
Stripe types.Object `tfsdk:"stripe"`
VoucherCustomized types.Bool `tfsdk:"voucher_customized"`
VoucherEnabled types.Bool `tfsdk:"voucher_enabled"`
WechatEnabled types.Bool `tfsdk:"wechat_enabled"`
Wechat types.Object `tfsdk:"wechat"`
}
type portalCustomizationModel struct {
Customized types.Bool `tfsdk:"customized"`
AuthenticationText types.String `tfsdk:"authentication_text"`
BgColor types.String `tfsdk:"bg_color"`
BgImageFileId types.String `tfsdk:"bg_image_file_id"`
BgImageTile types.Bool `tfsdk:"bg_image_tile"`
BgType types.String `tfsdk:"bg_type"`
BoxColor types.String `tfsdk:"box_color"`
BoxLinkColor types.String `tfsdk:"box_link_color"`
BoxOpacity types.Int32 `tfsdk:"box_opacity"`
BoxRadius types.Int32 `tfsdk:"box_radius"`
BoxTextColor types.String `tfsdk:"box_text_color"`
ButtonColor types.String `tfsdk:"button_color"`
ButtonText types.String `tfsdk:"button_text"`
ButtonTextColor types.String `tfsdk:"button_text_color"`
Languages types.List `tfsdk:"languages"`
LinkColor types.String `tfsdk:"link_color"`
LogoFileId types.String `tfsdk:"logo_file_id"`
LogoPosition types.String `tfsdk:"logo_position"`
LogoSize types.Int32 `tfsdk:"logo_size"`
SuccessText types.String `tfsdk:"success_text"`
TextColor types.String `tfsdk:"text_color"`
Title types.String `tfsdk:"title"`
Tos types.String `tfsdk:"tos"`
TosEnabled types.Bool `tfsdk:"tos_enabled"`
UnsplashAuthorName types.String `tfsdk:"unsplash_author_name"`
UnsplashAuthorUsername types.String `tfsdk:"unsplash_author_username"`
WelcomeText types.String `tfsdk:"welcome_text"`
WelcomeTextEnabled types.Bool `tfsdk:"welcome_text_enabled"`
WelcomeTextPosition types.String `tfsdk:"welcome_text_position"`
}
func (m *portalCustomizationModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"customized": types.BoolType,
"authentication_text": types.StringType,
"bg_color": types.StringType,
"bg_image_file_id": types.StringType,
"bg_image_tile": types.BoolType,
"bg_type": types.StringType,
"box_color": types.StringType,
"box_link_color": types.StringType,
"box_opacity": types.Int32Type,
"box_radius": types.Int32Type,
"box_text_color": types.StringType,
"button_color": types.StringType,
"button_text": types.StringType,
"button_text_color": types.StringType,
"languages": types.ListType{
ElemType: types.StringType,
},
"link_color": types.StringType,
"logo_file_id": types.StringType,
"logo_position": types.StringType,
"logo_size": types.Int32Type,
"success_text": types.StringType,
"text_color": types.StringType,
"title": types.StringType,
"tos": types.StringType,
"tos_enabled": types.BoolType,
"unsplash_author_name": types.StringType,
"unsplash_author_username": types.StringType,
"welcome_text": types.StringType,
"welcome_text_enabled": types.BoolType,
"welcome_text_position": types.StringType,
}
}
type facebookModel struct {
AppID types.String `tfsdk:"app_id"`
AppSecret types.String `tfsdk:"app_secret"`
ScopeEmail types.Bool `tfsdk:"scope_email"`
}
func (m *facebookModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"app_id": types.StringType,
"app_secret": types.StringType,
"scope_email": types.BoolType,
}
}
type facebookWifiModel struct {
BlockHttps types.Bool `tfsdk:"block_https"`
GwID types.String `tfsdk:"gateway_id"`
GwName types.String `tfsdk:"gateway_name"`
GwSecret types.String `tfsdk:"gateway_secret"`
}
func (m *facebookWifiModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"block_https": types.BoolType,
"gateway_id": types.StringType,
"gateway_name": types.StringType,
"gateway_secret": types.StringType,
}
}
type googleModel struct {
ClientID types.String `tfsdk:"client_id"`
ClientSecret types.String `tfsdk:"client_secret"`
Domain types.String `tfsdk:"domain"`
ScopeEmail types.Bool `tfsdk:"scope_email"`
}
func (m *googleModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"client_id": types.StringType,
"client_secret": types.StringType,
"domain": types.StringType,
"scope_email": types.BoolType,
}
}
type paypalModel struct {
Password types.String `tfsdk:"password"`
Username types.String `tfsdk:"username"`
UseSandbox types.Bool `tfsdk:"use_sandbox"`
Signature types.String `tfsdk:"signature"`
}
func (m *paypalModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"password": types.StringType,
"username": types.StringType,
"use_sandbox": types.BoolType,
"signature": types.StringType,
}
}
type ipPayModel struct {
UseSandbox types.Bool `tfsdk:"use_sandbox"`
TerminalID types.String `tfsdk:"terminal_id"`
}
func (m *ipPayModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"use_sandbox": types.BoolType,
"terminal_id": types.StringType,
}
}
type quickpayModel struct {
AgreementID types.String `tfsdk:"agreement_id"`
ApiKey types.String `tfsdk:"api_key"`
MerchantID types.String `tfsdk:"merchant_id"`
UseSandbox types.Bool `tfsdk:"use_sandbox"`
}
func (m *quickpayModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"agreement_id": types.StringType,
"api_key": types.StringType,
"merchant_id": types.StringType,
"use_sandbox": types.BoolType,
}
}
type radiusModel struct {
AuthType types.String `tfsdk:"auth_type"`
DisconnectEnabled types.Bool `tfsdk:"disconnect_enabled"`
DisconnectPort types.Int32 `tfsdk:"disconnect_port"`
ProfileID types.String `tfsdk:"profile_id"`
}
func (m *radiusModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"auth_type": types.StringType,
"disconnect_enabled": types.BoolType,
"disconnect_port": types.Int32Type,
"profile_id": types.StringType,
}
}
type redirectModel struct {
UseHttps types.Bool `tfsdk:"use_https"`
ToHttps types.Bool `tfsdk:"to_https"`
Url types.String `tfsdk:"url"`
}
func (m *redirectModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"use_https": types.BoolType,
"to_https": types.BoolType,
"url": types.StringType,
}
}
type wechatModel struct {
AppID types.String `tfsdk:"app_id"`
AppSecret types.String `tfsdk:"app_secret"`
ShopID types.String `tfsdk:"shop_id"`
SecretKey types.String `tfsdk:"secret_key"`
}
func (m *wechatModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"app_id": types.StringType,
"app_secret": types.StringType,
"shop_id": types.StringType,
"secret_key": types.StringType,
}
}
type authorizeModel struct {
LoginID types.String `tfsdk:"login_id"`
TransactionKey types.String `tfsdk:"transaction_key"`
UseSandbox types.Bool `tfsdk:"use_sandbox"`
}
func (m *authorizeModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"login_id": types.StringType,
"transaction_key": types.StringType,
"use_sandbox": types.BoolType,
}
}
type merchantWarriorModel struct {
ApiKey types.String `tfsdk:"api_key"`
ApiPassphrase types.String `tfsdk:"api_passphrase"`
MerchantID types.String `tfsdk:"merchant_uuid"`
UseSandbox types.Bool `tfsdk:"use_sandbox"`
}
func (m *merchantWarriorModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"api_key": types.StringType,
"api_passphrase": types.StringType,
"merchant_uuid": types.StringType,
"use_sandbox": types.BoolType,
}
}
type stripeModel struct {
ApiKey types.String `tfsdk:"api_key"`
}
func (m *stripeModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"api_key": types.StringType,
}
}
func (d *guestAccessModel) AsUnifiModel(ctx context.Context) (interface{}, diag.Diagnostics) {
var diags diag.Diagnostics
model := &unifi.SettingGuestAccess{
AllowedSubnet: d.AllowedSubnet.ValueString(),
RestrictedSubnet: d.RestrictedSubnet.ValueString(),
Auth: d.Auth.ValueString(),
AuthUrl: d.AuthUrl.ValueString(),
CustomIP: d.CustomIP.ValueString(),
EcEnabled: d.EcEnabled.ValueBool(),
Expire: int(d.Expire.ValueInt32()),
ExpireNumber: int(d.ExpireNumber.ValueInt32()),
ExpireUnit: int(d.ExpireUnit.ValueInt32()),
PortalEnabled: d.PortalEnabled.ValueBool(),
PortalHostname: d.PortalHostname.ValueString(),
PortalUseHostname: d.PortalUseHostname.ValueBool(),
TemplateEngine: d.TemplateEngine.ValueString(),
VoucherCustomized: d.VoucherCustomized.ValueBool(),
VoucherEnabled: d.VoucherEnabled.ValueBool(),
}
if ut.IsEmptyString(d.Password) {
model.PasswordEnabled = false
} else {
model.PasswordEnabled = true
model.XPassword = d.Password.ValueString()
}
diags = d.paymentAsUnifiModel(ctx, model)
if diags.HasError() {
return nil, diags
}
if ut.IsDefined(d.Redirect) {
var redirect *redirectModel
diags.Append(d.Redirect.As(ctx, &redirect, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.RedirectEnabled = true
model.RedirectUrl = redirect.Url.ValueString()
model.RedirectToHttps = redirect.ToHttps.ValueBool()
model.RedirectHttps = redirect.UseHttps.ValueBool()
} else {
model.RedirectEnabled = false
}
if ut.IsDefined(d.Facebook) {
var facebook *facebookModel
diags.Append(d.Facebook.As(ctx, &facebook, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.FacebookEnabled = true
model.FacebookAppID = facebook.AppID.ValueString()
model.XFacebookAppSecret = facebook.AppSecret.ValueString()
model.FacebookScopeEmail = facebook.ScopeEmail.ValueBool()
} else {
model.FacebookEnabled = false
}
if ut.IsDefined(d.Google) {
var google *googleModel
diags.Append(d.Google.As(ctx, &google, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.GoogleEnabled = true
model.GoogleClientID = google.ClientID.ValueString()
model.XGoogleClientSecret = google.ClientSecret.ValueString()
model.GoogleScopeEmail = google.ScopeEmail.ValueBool()
model.GoogleDomain = google.Domain.ValueString()
} else {
model.GoogleEnabled = false
}
if ut.IsDefined(d.Radius) {
var radius *radiusModel
diags.Append(d.Radius.As(ctx, &radius, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.RADIUSEnabled = true
model.RADIUSAuthType = radius.AuthType.ValueString()
model.RADIUSDisconnectEnabled = radius.DisconnectEnabled.ValueBool()
model.RADIUSDisconnectPort = int(radius.DisconnectPort.ValueInt32())
model.RADIUSProfileID = radius.ProfileID.ValueString()
} else {
model.RADIUSEnabled = false
}
if ut.IsDefined(d.Wechat) {
var wechat *wechatModel
diags.Append(d.Wechat.As(ctx, &wechat, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.WechatEnabled = true
model.WechatAppID = wechat.AppID.ValueString()
model.XWechatAppSecret = wechat.AppSecret.ValueString()
model.WechatShopID = wechat.ShopID.ValueString()
model.XWechatSecretKey = wechat.SecretKey.ValueString()
} else {
model.WechatEnabled = false
}
if ut.IsDefined(d.FacebookWifi) {
var facebookWifi *facebookWifiModel
diags.Append(d.FacebookWifi.As(ctx, &facebookWifi, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
model.FacebookWifiBlockHttps = facebookWifi.BlockHttps.ValueBool()
model.FacebookWifiGwID = facebookWifi.GwID.ValueString()
model.FacebookWifiGwName = facebookWifi.GwName.ValueString()
model.XFacebookWifiGwSecret = facebookWifi.GwSecret.ValueString()
}
if ut.IsDefined(d.RestrictedDNSServers) {
var servers []string
diags.Append(ut.ListElementsAs(d.RestrictedDNSServers, &servers)...)
if diags.HasError() {
return nil, diags
}
if len(servers) > 0 {
model.RestrictedDNSEnabled = true
}
model.RestrictedDNSServers = servers
} else {
model.RestrictedDNSEnabled = false
}
if ut.IsDefined(d.PortalCustomization) {
var portalCustomization *portalCustomizationModel
diags.Append(d.PortalCustomization.As(ctx, &portalCustomization, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
var languages []string
diags := ut.ListElementsAs(portalCustomization.Languages, &languages)
if diags.HasError() {
return nil, diags
}
model.PortalCustomized = portalCustomization.Customized.ValueBool()
model.PortalCustomizedAuthenticationText = portalCustomization.AuthenticationText.ValueString()
model.PortalCustomizedBgColor = portalCustomization.BgColor.ValueString()
model.PortalCustomizedBgImageFilename = portalCustomization.BgImageFileId.ValueString()
model.PortalCustomizedBgImageTile = portalCustomization.BgImageTile.ValueBool()
model.PortalCustomizedBgType = portalCustomization.BgType.ValueString()
model.PortalCustomizedBoxColor = portalCustomization.BoxColor.ValueString()
model.PortalCustomizedBoxLinkColor = portalCustomization.BoxLinkColor.ValueString()
model.PortalCustomizedBoxOpacity = int(portalCustomization.BoxOpacity.ValueInt32())
model.PortalCustomizedBoxRADIUS = int(portalCustomization.BoxRadius.ValueInt32())
model.PortalCustomizedBoxTextColor = portalCustomization.BoxTextColor.ValueString()
model.PortalCustomizedButtonColor = portalCustomization.ButtonColor.ValueString()
model.PortalCustomizedButtonText = portalCustomization.ButtonText.ValueString()
model.PortalCustomizedButtonTextColor = portalCustomization.ButtonTextColor.ValueString()
model.PortalCustomizedLanguages = languages
model.PortalCustomizedLinkColor = portalCustomization.LinkColor.ValueString()
model.PortalCustomizedLogoFilename = portalCustomization.LogoFileId.ValueString()
model.PortalCustomizedLogoPosition = portalCustomization.LogoPosition.ValueString()
model.PortalCustomizedLogoSize = int(portalCustomization.LogoSize.ValueInt32())
model.PortalCustomizedSuccessText = portalCustomization.SuccessText.ValueString()
model.PortalCustomizedTextColor = portalCustomization.TextColor.ValueString()
model.PortalCustomizedTitle = portalCustomization.Title.ValueString()
model.PortalCustomizedTos = portalCustomization.Tos.ValueString()
model.PortalCustomizedTosEnabled = portalCustomization.TosEnabled.ValueBool()
model.PortalCustomizedUnsplashAuthorName = portalCustomization.UnsplashAuthorName.ValueString()
model.PortalCustomizedUnsplashAuthorUsername = portalCustomization.UnsplashAuthorUsername.ValueString()
model.PortalCustomizedWelcomeText = portalCustomization.WelcomeText.ValueString()
model.PortalCustomizedWelcomeTextEnabled = portalCustomization.WelcomeTextEnabled.ValueBool()
model.PortalCustomizedWelcomeTextPosition = portalCustomization.WelcomeTextPosition.ValueString()
} else {
model.PortalCustomized = false
}
return model, diags
}
func (d *guestAccessModel) paymentAsUnifiModel(ctx context.Context, model *unifi.SettingGuestAccess) diag.Diagnostics {
diags := diag.Diagnostics{}
if ut.IsEmptyString(d.PaymentGateway) {
model.PaymentEnabled = false
} else {
gateway := d.PaymentGateway.ValueString()
model.PaymentEnabled = true
model.Gateway = gateway
switch gateway {
case "authorize":
var authorize *authorizeModel
diags.Append(d.Authorize.As(ctx, &authorize, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
if ut.IsDefined(authorize.UseSandbox) {
model.AuthorizeUseSandbox = authorize.UseSandbox.ValueBool()
}
model.XAuthorizeLoginid = authorize.LoginID.ValueString()
model.XAuthorizeTransactionkey = authorize.TransactionKey.ValueString()
case "ippay":
var ippay *ipPayModel
diags.Append(d.IPpay.As(ctx, &ippay, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
if ut.IsDefined(ippay.UseSandbox) {
model.IPpayUseSandbox = ippay.UseSandbox.ValueBool()
}
model.XIPpayTerminalid = ippay.TerminalID.ValueString()
case "merchantwarrior":
var merchantWarrior *merchantWarriorModel
diags.Append(d.MerchantWarrior.As(ctx, &merchantWarrior, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
if ut.IsDefined(merchantWarrior.UseSandbox) {
model.MerchantwarriorUseSandbox = merchantWarrior.UseSandbox.ValueBool()
}
model.XMerchantwarriorApikey = merchantWarrior.ApiKey.ValueString()
model.XMerchantwarriorApipassphrase = merchantWarrior.ApiPassphrase.ValueString()
model.XMerchantwarriorMerchantuuid = merchantWarrior.MerchantID.ValueString()
case "paypal":
var paypal *paypalModel
diags.Append(d.Paypal.As(ctx, &paypal, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
if ut.IsDefined(paypal.UseSandbox) {
model.PaypalUseSandbox = paypal.UseSandbox.ValueBool()
}
model.XPaypalPassword = paypal.Password.ValueString()
model.XPaypalUsername = paypal.Username.ValueString()
model.XPaypalSignature = paypal.Signature.ValueString()
case "quickpay":
var quickpay *quickpayModel
diags.Append(d.Quickpay.As(ctx, &quickpay, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
if ut.IsDefined(quickpay.UseSandbox) {
model.QuickpayTestmode = quickpay.UseSandbox.ValueBool()
}
model.XQuickpayAgreementid = quickpay.AgreementID.ValueString()
model.XQuickpayApikey = quickpay.ApiKey.ValueString()
model.XQuickpayMerchantid = quickpay.MerchantID.ValueString()
case "stripe":
var stripe *stripeModel
diags.Append(d.Stripe.As(ctx, &stripe, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return diags
}
model.XStripeApiKey = stripe.ApiKey.ValueString()
default:
diags.AddError("Invalid payment gateway", fmt.Sprintf("Payment gateway %q is not supported", gateway))
}
}
return diags
}
func (d *guestAccessModel) mergePaymentModel(ctx context.Context, model *unifi.SettingGuestAccess) diag.Diagnostics {
diags := diag.Diagnostics{}
switch model.Gateway {
case "authorize":
authorize := &authorizeModel{
LoginID: types.StringValue(model.XAuthorizeLoginid),
TransactionKey: types.StringValue(model.XAuthorizeTransactionkey),
UseSandbox: types.BoolValue(model.AuthorizeUseSandbox),
}
d.Authorize, diags = types.ObjectValueFrom(ctx, authorize.AttributeTypes(), authorize)
case "ippay":
ippay := &ipPayModel{
UseSandbox: types.BoolValue(model.IPpayUseSandbox),
TerminalID: types.StringValue(model.XIPpayTerminalid),
}
d.IPpay, diags = types.ObjectValueFrom(ctx, ippay.AttributeTypes(), ippay)
case "merchantwarrior":
merchantWarrior := &merchantWarriorModel{
ApiKey: types.StringValue(model.XMerchantwarriorApikey),
ApiPassphrase: types.StringValue(model.XMerchantwarriorApipassphrase),
MerchantID: types.StringValue(model.XMerchantwarriorMerchantuuid),
UseSandbox: types.BoolValue(model.MerchantwarriorUseSandbox),
}
d.MerchantWarrior, diags = types.ObjectValueFrom(ctx, merchantWarrior.AttributeTypes(), merchantWarrior)
case "paypal":
paypal := &paypalModel{
Password: types.StringValue(model.XPaypalPassword),
Username: types.StringValue(model.XPaypalUsername),
UseSandbox: types.BoolValue(model.PaypalUseSandbox),
Signature: types.StringValue(model.XPaypalSignature),
}
d.Paypal, diags = types.ObjectValueFrom(ctx, paypal.AttributeTypes(), paypal)
case "quickpay":
quickpay := &quickpayModel{
AgreementID: types.StringValue(model.XQuickpayAgreementid),
ApiKey: types.StringValue(model.XQuickpayApikey),
MerchantID: types.StringValue(model.XQuickpayMerchantid),
UseSandbox: types.BoolValue(model.QuickpayTestmode),
}
d.Quickpay, diags = types.ObjectValueFrom(ctx, quickpay.AttributeTypes(), quickpay)
case "stripe":
stripe := &stripeModel{
ApiKey: types.StringValue(model.XStripeApiKey),
}
d.Stripe, diags = types.ObjectValueFrom(ctx, stripe.AttributeTypes(), stripe)
default:
diags.AddError("Invalid payment gateway", fmt.Sprintf("Payment gateway returned by controller is not supported: %s", model.Gateway))
}
return diags
}
func (d *guestAccessModel) Merge(ctx context.Context, unifiModel interface{}) diag.Diagnostics {
diags := diag.Diagnostics{}
model, ok := unifiModel.(*unifi.SettingGuestAccess)
if !ok {
diags.AddError("Invalid model type", "Expected *unifi.SettingGuestAccess")
return diags
}
d.ID = types.StringValue(model.ID)
d.AllowedSubnet = types.StringValue(model.AllowedSubnet)
d.RestrictedSubnet = types.StringValue(model.RestrictedSubnet)
d.Auth = types.StringValue(model.Auth)
d.AuthUrl = types.StringValue(model.AuthUrl)
switch model.Auth {
case "custom":
d.CustomIP = types.StringValue(model.CustomIP)
default:
d.CustomIP = types.StringNull()
}
d.EcEnabled = types.BoolValue(model.EcEnabled)
d.Expire = types.Int32Value(int32(model.Expire))
d.ExpireNumber = types.Int32Value(int32(model.ExpireNumber))
d.ExpireUnit = types.Int32Value(int32(model.ExpireUnit))
d.PaymentEnabled = types.BoolValue(model.PaymentEnabled)
var od diag.Diagnostics
d.Authorize, od = ut.ObjectNull(&authorizeModel{})
diags.Append(od...)
d.Paypal, od = ut.ObjectNull(&paypalModel{})
diags.Append(od...)
d.IPpay, od = ut.ObjectNull(&ipPayModel{})
diags.Append(od...)
d.MerchantWarrior, od = ut.ObjectNull(&merchantWarriorModel{})
diags.Append(od...)
d.Quickpay, od = ut.ObjectNull(&quickpayModel{})
diags.Append(od...)
d.Stripe, od = ut.ObjectNull(&stripeModel{})
diags.Append(od...)
if diags.HasError() {
return diags
}
if model.PaymentEnabled {
d.PaymentGateway = types.StringValue(model.Gateway)
d.mergePaymentModel(ctx, model)
} else {
d.PaymentGateway = types.StringNull()
}
d.PasswordEnabled = types.BoolValue(model.PasswordEnabled)
if model.PasswordEnabled {
d.Password = types.StringValue(model.XPassword)
} else {
d.Password = types.StringNull()
}
d.RedirectEnabled = types.BoolValue(model.RedirectEnabled)
d.Redirect, diags = ut.ObjectNull(&redirectModel{})
if diags.HasError() {
return diags
}
if model.RedirectEnabled {
redirect := &redirectModel{
UseHttps: types.BoolValue(model.RedirectHttps),
ToHttps: types.BoolValue(model.RedirectToHttps),
Url: types.StringValue(model.RedirectUrl),
}
d.Redirect, diags = types.ObjectValueFrom(ctx, redirect.AttributeTypes(), redirect)
if diags.HasError() {
return diags
}
}
d.FacebookEnabled = types.BoolValue(model.FacebookEnabled)
d.Facebook, diags = ut.ObjectNull(&facebookModel{})
if diags.HasError() {
return diags
}
if model.FacebookEnabled {
facebook := &facebookModel{
AppID: types.StringValue(model.FacebookAppID),
AppSecret: types.StringValue(model.XFacebookAppSecret),
ScopeEmail: types.BoolValue(model.FacebookScopeEmail),
}
d.Facebook, diags = types.ObjectValueFrom(ctx, facebook.AttributeTypes(), facebook)
if diags.HasError() {
return diags
}
}
d.GoogleEnabled = types.BoolValue(model.GoogleEnabled)
d.Google, diags = ut.ObjectNull(&googleModel{})
if diags.HasError() {
return diags
}
if model.GoogleEnabled {
google := &googleModel{
ClientID: types.StringValue(model.GoogleClientID),
ClientSecret: types.StringValue(model.XGoogleClientSecret),
Domain: types.StringValue(model.GoogleDomain),
ScopeEmail: types.BoolValue(model.GoogleScopeEmail),
}
d.Google, diags = types.ObjectValueFrom(ctx, google.AttributeTypes(), google)
if diags.HasError() {
return diags
}
}
d.RadiusEnabled = types.BoolValue(model.RADIUSEnabled)
d.Radius, diags = ut.ObjectNull(&radiusModel{})
if diags.HasError() {
return diags
}
if model.RADIUSEnabled {
radius := &radiusModel{
AuthType: types.StringValue(model.RADIUSAuthType),
DisconnectEnabled: types.BoolValue(model.RADIUSDisconnectEnabled),
DisconnectPort: types.Int32Value(int32(model.RADIUSDisconnectPort)),
ProfileID: types.StringValue(model.RADIUSProfileID),
}
d.Radius, diags = types.ObjectValueFrom(ctx, radius.AttributeTypes(), radius)
if diags.HasError() {
return diags
}
}
d.WechatEnabled = types.BoolValue(model.WechatEnabled)
d.Wechat, diags = ut.ObjectNull(&wechatModel{})
if diags.HasError() {
return diags
}
if model.WechatEnabled {
wechat := &wechatModel{
AppID: types.StringValue(model.WechatAppID),
ShopID: types.StringValue(model.WechatShopID),
AppSecret: types.StringValue(model.XWechatAppSecret),
SecretKey: types.StringValue(model.XWechatSecretKey),
}
d.Wechat, diags = types.ObjectValueFrom(ctx, wechat.AttributeTypes(), wechat)
if diags.HasError() {
return diags
}
}
d.FacebookWifi, diags = ut.ObjectNull(&facebookWifiModel{})
if diags.HasError() {
return diags
}
if model.Auth == "facebook_wifi" {
facebookWifi := &facebookWifiModel{
BlockHttps: types.BoolValue(model.FacebookWifiBlockHttps),
GwID: types.StringValue(model.FacebookWifiGwID),
GwName: types.StringValue(model.FacebookWifiGwName),
GwSecret: types.StringValue(model.XFacebookWifiGwSecret),
}
d.FacebookWifi, diags = types.ObjectValueFrom(ctx, facebookWifi.AttributeTypes(), facebookWifi)
if diags.HasError() {
return diags
}
}
d.RestrictedDNSEnabled = types.BoolValue(model.RestrictedDNSEnabled)
if model.RestrictedDNSEnabled && len(model.RestrictedDNSServers) > 0 {
d.RestrictedDNSServers, diags = types.ListValueFrom(ctx, types.StringType, model.RestrictedDNSServers)
if diags.HasError() {
return diags
}
} else {
d.RestrictedDNSServers = ut.EmptyList(types.StringType)
}
languages, diags := types.ListValueFrom(ctx, types.StringType, model.PortalCustomizedLanguages)
customizations := &portalCustomizationModel{
Customized: types.BoolValue(model.PortalCustomized),
AuthenticationText: types.StringValue(model.PortalCustomizedAuthenticationText),
BgColor: types.StringValue(model.PortalCustomizedBgColor),
BgImageFileId: types.StringValue(model.PortalCustomizedBgImageFilename),
BgImageTile: types.BoolValue(model.PortalCustomizedBgImageTile),
BgType: types.StringValue(model.PortalCustomizedBgType),
BoxColor: types.StringValue(model.PortalCustomizedBoxColor),
BoxLinkColor: types.StringValue(model.PortalCustomizedBoxLinkColor),
BoxOpacity: types.Int32Value(int32(model.PortalCustomizedBoxOpacity)),
BoxRadius: types.Int32Value(int32(model.PortalCustomizedBoxRADIUS)),
BoxTextColor: types.StringValue(model.PortalCustomizedBoxTextColor),
ButtonColor: types.StringValue(model.PortalCustomizedButtonColor),
ButtonText: types.StringValue(model.PortalCustomizedButtonText),
ButtonTextColor: types.StringValue(model.PortalCustomizedButtonTextColor),
Languages: languages,
LinkColor: types.StringValue(model.PortalCustomizedLinkColor),
LogoFileId: types.StringValue(model.PortalCustomizedLogoFilename),
LogoPosition: types.StringValue(model.PortalCustomizedLogoPosition),
LogoSize: types.Int32Value(int32(model.PortalCustomizedLogoSize)),
SuccessText: types.StringValue(model.PortalCustomizedSuccessText),
TextColor: types.StringValue(model.PortalCustomizedTextColor),
Title: types.StringValue(model.PortalCustomizedTitle),
Tos: types.StringValue(model.PortalCustomizedTos),
TosEnabled: types.BoolValue(model.PortalCustomizedTosEnabled),
UnsplashAuthorName: types.StringValue(model.PortalCustomizedUnsplashAuthorName),
UnsplashAuthorUsername: types.StringValue(model.PortalCustomizedUnsplashAuthorUsername),
WelcomeText: types.StringValue(model.PortalCustomizedWelcomeText),
WelcomeTextEnabled: types.BoolValue(model.PortalCustomizedWelcomeTextEnabled),
WelcomeTextPosition: types.StringValue(model.PortalCustomizedWelcomeTextPosition),
}
d.PortalCustomization, diags = types.ObjectValueFrom(ctx, customizations.AttributeTypes(), customizations)
if diags.HasError() {
return diags
}
d.PortalEnabled = types.BoolValue(model.PortalEnabled)
d.PortalHostname = types.StringValue(model.PortalHostname)
d.PortalUseHostname = types.BoolValue(model.PortalUseHostname)
d.TemplateEngine = types.StringValue(model.TemplateEngine)
d.VoucherCustomized = types.BoolValue(model.VoucherCustomized)
d.VoucherEnabled = types.BoolValue(model.VoucherEnabled)
return diags
}
var (
_ resource.Resource = &guestAccessResource{}
_ resource.ResourceWithConfigure = &guestAccessResource{}
_ resource.ResourceWithImportState = &guestAccessResource{}
_ resource.ResourceWithConfigValidators = &guestAccessResource{}
_ resource.ResourceWithModifyPlan = &guestAccessResource{}
_ base.Resource = &guestAccessResource{}
)
type guestAccessResource struct {
*base.GenericResource[*guestAccessModel]
}
func (g *guestAccessResource) ModifyPlan(_ context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("bg_type"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("box_radius"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("button_text"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("success_text"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("authentication_text"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("logo_size"), req.Config)...)
resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("logo_position"), req.Config)...)
}
func requiredTogetherIfStringVal(condition, value string, attrs ...string) validators.RequiredTogetherIfValidator {
var expressions []path.Expression
for _, attr := range attrs {
expressions = append(expressions, path.MatchRoot(attr))
}
return validators.RequiredTogetherIf(path.MatchRoot(condition), types.StringValue(value), expressions...)
}
func requiredStringValueIfTrue(conditionAttr, targetAttr, targetVal string) validators.RequiredValueIfValidator {
return validators.RequiredValueIf(path.MatchRoot(conditionAttr), types.BoolValue(true), path.MatchRoot(targetAttr), types.StringValue(targetVal))
}
func (g *guestAccessResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{
// Auth validators
requiredTogetherIfStringVal("auth", "custom", "custom_ip"),
// Payment validators
requiredTogetherIfStringVal("payment_gateway", "authorize", "authorize"),
requiredTogetherIfStringVal("payment_gateway", "ippay", "ippay"),
requiredTogetherIfStringVal("payment_gateway", "merchantwarrior", "merchant_warrior"),
requiredTogetherIfStringVal("payment_gateway", "paypal", "paypal"),
requiredTogetherIfStringVal("payment_gateway", "quickpay", "quickpay"),
requiredTogetherIfStringVal("payment_gateway", "stripe", "stripe"),
// Portal validators
requiredTogetherIfStringVal("portal_customization.bg_type", "image", "portal_customization.bg_image_file_id"),
// Voucher validators
requiredStringValueIfTrue("voucher_enabled", "auth", "hotspot"),
}
}
func (g *guestAccessResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The `unifi_setting_guest_access` resource manages the guest access settings in the UniFi controller.\n\nThis resource allows you to configure all aspects of guest network access including authentication methods, portal customization, and payment options.",
Attributes: map[string]schema.Attribute{
"id": ut.ID(),
"site": ut.SiteAttribute(),
"allowed_subnet": schema.StringAttribute{
MarkdownDescription: "Subnet allowed for guest access.",
Optional: true,
Computed: true,
},
"restricted_subnet": schema.StringAttribute{
MarkdownDescription: "Subnet for restricted guest access.",
Optional: true,
Computed: true,
},
"auth": schema.StringAttribute{
MarkdownDescription: "Authentication method for guest access. Valid values are:\n" +
"* `none` - No authentication required\n" +
"* `hotspot` - Password authentication\n" +
"* `facebook_wifi` - Facebook auth entication\n" +
"* `custom` - Custom authentication\n\n" +
"For password authentication, set `auth` to `hotspot` and `password_enabled` to `true`.\n" +
"For voucher authentication, set `auth` to `hotspot` and `voucher_enabled` to `true`.\n" +
"For payment authentication, set `auth` to `hotspot` and `payment_enabled` to `true`.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("none"),
Validators: []validator.String{
stringvalidator.OneOf("none", "hotspot", "facebook_wifi", "custom"),
},
},
"auth_url": schema.StringAttribute{
MarkdownDescription: "URL for authentication. Must be a valid URL including the protocol.",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.URL(),
},
},
"authorize": schema.SingleNestedAttribute{
MarkdownDescription: "Authorize.net payment settings.",
Optional: true,
Validators: []validator.Object{},
Attributes: map[string]schema.Attribute{
"use_sandbox": schema.BoolAttribute{
MarkdownDescription: "Use sandbox mode for Authorize.net payments.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"login_id": schema.StringAttribute{
MarkdownDescription: "Authorize.net login ID for authentication.",
Required: true,
},
"transaction_key": schema.StringAttribute{
MarkdownDescription: "Authorize.net transaction key for authentication.",
Required: true,
},
},
},
"custom_ip": schema.StringAttribute{
MarkdownDescription: "Custom IP address. Must be a valid IPv4 address (e.g., `192.168.1.1`).",
Optional: true,
Validators: []validator.String{
validators.IPv4(),
},
},
"ec_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable enterprise controller functionality.",
Optional: true,
Computed: true,
},
"expire": schema.Int32Attribute{
MarkdownDescription: "Expiration time for guest access.",
Optional: true,
Computed: true,
},
"expire_number": schema.Int32Attribute{
MarkdownDescription: "Number value for the expiration time.",
Optional: true,
Computed: true,
},
"expire_unit": schema.Int32Attribute{
MarkdownDescription: "Unit for the expiration time. Valid values are:\n" +
"* `1` - Minute\n" +
"* `60` - Hour\n" +
"* `1440` - Day\n" +
"* `10080` - Week",
Optional: true,
Computed: true,
Validators: []validator.Int32{
int32validator.OneOf(1, 60, 1440, 10080),
},
},
"facebook_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether Facebook authentication for guest access is enabled.",
Computed: true,
},
"facebook": schema.SingleNestedAttribute{
MarkdownDescription: "Facebook authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"app_id": schema.StringAttribute{
MarkdownDescription: "Facebook application ID for authentication.",
Required: true,
},
"app_secret": schema.StringAttribute{
MarkdownDescription: "Facebook application secret for authentication.",
Required: true,
Sensitive: true,
},
"scope_email": schema.BoolAttribute{
MarkdownDescription: "Request email scope for Facebook authentication.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
},
"facebook_wifi": schema.SingleNestedAttribute{
MarkdownDescription: "Facebook WiFi authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"block_https": schema.BoolAttribute{
MarkdownDescription: "Mode HTTPS for Facebook WiFi.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"gateway_id": schema.StringAttribute{
MarkdownDescription: "Facebook WiFi gateway ID.",
Required: true,
},
"gateway_name": schema.StringAttribute{
MarkdownDescription: "Facebook WiFi gateway name.",
Required: true,
},
"gateway_secret": schema.StringAttribute{
MarkdownDescription: "Facebook WiFi gateway secret.",
Required: true,
Sensitive: true,
},
},
},
"google_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether Google authentication for guest access is enabled.",
Computed: true,
},
"google": schema.SingleNestedAttribute{
MarkdownDescription: "Google authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"client_id": schema.StringAttribute{
MarkdownDescription: "Google client ID for authentication.",
Required: true,
//Sensitive: true,
},
"client_secret": schema.StringAttribute{
MarkdownDescription: "Google client secret for authentication.",
Required: true,
//Sensitive: true,
},
"domain": schema.StringAttribute{
MarkdownDescription: "Restrict Google authentication to specific domain.",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.Hostname(),
},
},
"scope_email": schema.BoolAttribute{
MarkdownDescription: "Request email scope for Google authentication.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
},
"ippay": schema.SingleNestedAttribute{
MarkdownDescription: "IPpay Payments settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"terminal_id": schema.StringAttribute{
MarkdownDescription: "Terminal ID for IP Payments.",
Required: true,
Sensitive: true,
},
"use_sandbox": schema.BoolAttribute{
MarkdownDescription: "Whether to use sandbox mode for IPPay payments.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
},
},
"merchant_warrior": schema.SingleNestedAttribute{
MarkdownDescription: "MerchantWarrior payment settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"api_key": schema.StringAttribute{
MarkdownDescription: "MerchantWarrior API key.",
Required: true,
Sensitive: true,
},
"api_passphrase": schema.StringAttribute{
MarkdownDescription: "MerchantWarrior API passphrase.",
Required: true,
Sensitive: true,
},
"merchant_uuid": schema.StringAttribute{
MarkdownDescription: "MerchantWarrior merchant UUID.",
Required: true,
Sensitive: true,
},
"use_sandbox": schema.BoolAttribute{
MarkdownDescription: "Whether to use sandbox mode for MerchantWarrior payments.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
},
},
"paypal": schema.SingleNestedAttribute{
MarkdownDescription: "PayPal payment settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"password": schema.StringAttribute{
MarkdownDescription: "PayPal password.",
Required: true,
Sensitive: true,
},
"signature": schema.StringAttribute{
MarkdownDescription: "PayPal signature.",
Required: true,
Sensitive: true,
},
"use_sandbox": schema.BoolAttribute{
MarkdownDescription: "Whether to use sandbox mode for PayPal payments.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"username": schema.StringAttribute{
MarkdownDescription: "PayPal username. Must be a valid email address.",
Required: true,
Sensitive: true,
Validators: []validator.String{
validators.Email,
},
},
},
},
"password_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable password authentication for guest access.",
Computed: true,
},
"password": schema.StringAttribute{
MarkdownDescription: "Password for guest access.",
Optional: true,
Sensitive: true,
},
"payment_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable payment for guest access.",
Computed: true,
},
"payment_gateway": schema.StringAttribute{
MarkdownDescription: "Payment gateway. Valid values are:\n" +
"* `paypal` - PayPal\n" +
"* `stripe` - Stripe\n" +
"* `authorize` - Authorize.net\n" +
"* `quickpay` - QuickPay\n" +
"* `merchantwarrior` - Merchant Warrior\n" +
"* `ippay` - IP Payments",
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("paypal", "stripe", "authorize", "quickpay", "merchantwarrior", "ippay"),
},
},
"portal_customization": schema.SingleNestedAttribute{
MarkdownDescription: "Portal customization settings.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"customized": schema.BoolAttribute{
MarkdownDescription: "Whether the portal is customized.",
Optional: true,
Computed: true,
},
"authentication_text": schema.StringAttribute{
MarkdownDescription: "Custom authentication text for the portal.",
Optional: true,
Computed: true,
},
"bg_color": schema.StringAttribute{
MarkdownDescription: "Background color for the custom portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"bg_image_file_id": schema.StringAttribute{
MarkdownDescription: "ID of the background image portal file. File must exist in controller, use `unifi_portal_file` to manage it.",
Optional: true,
Computed: true,
},
"bg_image_tile": schema.BoolAttribute{
MarkdownDescription: "Tile the background image.",
Optional: true,
Computed: true,
},
"bg_type": schema.StringAttribute{
MarkdownDescription: "Type of portal background. Valid values are:\n" +
"* `color` - Solid color background\n" +
"* `image` - (not yet supported!) Custom image background\n" +
"* `gallery` - Image from Unsplash gallery",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf("color", "image", "gallery"),
},
},
"box_color": schema.StringAttribute{
MarkdownDescription: "Color of the login box in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"box_link_color": schema.StringAttribute{
MarkdownDescription: "Color of links in the login box. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"box_opacity": schema.Int32Attribute{
MarkdownDescription: "Opacity of the login box (0-100).",
Optional: true,
Computed: true,
Validators: []validator.Int32{
int32validator.Between(0, 100),
},
},
"box_radius": schema.Int32Attribute{
MarkdownDescription: "Border radius of the login box in pixels.",
Optional: true,
Computed: true,
Validators: []validator.Int32{
int32validator.AtLeast(0),
},
},
"box_text_color": schema.StringAttribute{
MarkdownDescription: "Text color in the login box. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"button_color": schema.StringAttribute{
MarkdownDescription: "Button color in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"button_text": schema.StringAttribute{
MarkdownDescription: "Custom text for the login button.",
Optional: true,
Computed: true,
},
"button_text_color": schema.StringAttribute{
MarkdownDescription: "Button text color. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"languages": schema.ListAttribute{
MarkdownDescription: "List of enabled languages for the portal.",
Optional: true,
Computed: true,
ElementType: types.StringType,
},
"link_color": schema.StringAttribute{
MarkdownDescription: "Color for links in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"logo_file_id": schema.StringAttribute{
MarkdownDescription: "ID of the logo image portal file. File must exist in controller, use `unifi_portal_file` to manage it.",
Optional: true,
Computed: true,
},
"logo_position": schema.StringAttribute{
MarkdownDescription: "Position of the logo in the portal. Valid values are: left, center, right.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf("left", "center", "right"),
},
},
"logo_size": schema.Int32Attribute{
MarkdownDescription: "Size of the logo in pixels.",
Optional: true,
Computed: true,
Validators: []validator.Int32{
int32validator.AtLeast(0),
},
},
"success_text": schema.StringAttribute{
MarkdownDescription: "Text displayed after successful authentication.",
Optional: true,
Computed: true,
},
"text_color": schema.StringAttribute{
MarkdownDescription: "Main text color for the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.HexColor,
},
},
"title": schema.StringAttribute{
MarkdownDescription: "Title of the portal page.",
Optional: true,
Computed: true,
},
"tos": schema.StringAttribute{
MarkdownDescription: "Terms of service text.",
Optional: true,
Computed: true,
},
"tos_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable terms of service acceptance requirement.",
Optional: true,
Computed: true,
},
"unsplash_author_name": schema.StringAttribute{
MarkdownDescription: "Name of the Unsplash author for gallery background.",
Optional: true,
Computed: true,
},
"unsplash_author_username": schema.StringAttribute{
MarkdownDescription: "Username of the Unsplash author for gallery background.",
Optional: true,
Computed: true,
},
"welcome_text": schema.StringAttribute{
MarkdownDescription: "Welcome text displayed on the portal.",
Optional: true,
Computed: true,
},
"welcome_text_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable welcome text display.",
Optional: true,
Computed: true,
},
"welcome_text_position": schema.StringAttribute{
MarkdownDescription: "Position of the welcome text. Valid values are: `under_logo`, `above_boxes`.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf("under_logo", "above_boxes"),
},
},
},
},
"portal_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable the guest portal.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"portal_hostname": schema.StringAttribute{
MarkdownDescription: "Hostname to use for the captive portal.",
Optional: true,
Computed: true,
Validators: []validator.String{
validators.Hostname(),
},
},
"portal_use_hostname": schema.BoolAttribute{
MarkdownDescription: "Use a custom hostname for the portal.",
Optional: true,
Computed: true,
},
"quickpay": schema.SingleNestedAttribute{
MarkdownDescription: "QuickPay payment settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"agreement_id": schema.StringAttribute{
MarkdownDescription: "QuickPay agreement ID.",
Required: true,
Sensitive: true,
},
"api_key": schema.StringAttribute{
MarkdownDescription: "QuickPay API key.",
Required: true,
Sensitive: true,
},
"merchant_id": schema.StringAttribute{
MarkdownDescription: "QuickPay merchant ID.",
Required: true,
Sensitive: true,
},
"use_sandbox": schema.BoolAttribute{
MarkdownDescription: "Enable sandbox mode for QuickPay payments.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
},
},
"radius_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether RADIUS authentication for guest access is enabled.",
Computed: true,
},
"radius": schema.SingleNestedAttribute{
MarkdownDescription: "RADIUS authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"auth_type": schema.StringAttribute{
MarkdownDescription: "RADIUS authentication type. Valid values are: `chap`, `mschapv2`.",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("chap", "mschapv2"),
},
},
"disconnect_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable RADIUS disconnect messages.",
Optional: true,
Computed: true,
},
"disconnect_port": schema.Int32Attribute{
MarkdownDescription: "Port for RADIUS disconnect messages.",
Optional: true,
Computed: true,
Validators: []validator.Int32{
int32validator.Between(1, 65535),
},
},
"profile_id": schema.StringAttribute{
MarkdownDescription: "ID of the RADIUS profile to use.",
Required: true,
},
},
},
"redirect_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether redirect after authentication is enabled.",
Computed: true,
},
"redirect": schema.SingleNestedAttribute{
MarkdownDescription: "Redirect after authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"use_https": schema.BoolAttribute{
MarkdownDescription: "Use HTTPS for the redirect URL.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"to_https": schema.BoolAttribute{
MarkdownDescription: "Redirect HTTP requests to HTTPS.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"url": schema.StringAttribute{
MarkdownDescription: "URL to redirect to after authentication. Must be a valid URL.",
Required: true,
Validators: []validator.String{
validators.URL(),
},
},
},
},
"restricted_dns_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether restricted DNS servers for guest networks are enabled.",
Computed: true,
},
"restricted_dns_servers": schema.ListAttribute{
MarkdownDescription: "List of restricted DNS servers for guest networks. Each value must be a valid IPv4 address.",
Optional: true,
Computed: true,
ElementType: types.StringType,
Default: listdefault.StaticValue(ut.EmptyList(types.StringType)),
Validators: []validator.List{
listvalidator.ValueStringsAre(validators.IPv4()),
},
},
"stripe": schema.SingleNestedAttribute{
MarkdownDescription: "Stripe payment settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"api_key": schema.StringAttribute{
MarkdownDescription: "Stripe API key.",
Required: true,
Sensitive: true,
},
},
},
"template_engine": schema.StringAttribute{
MarkdownDescription: "Template engine for the portal. Valid values are: `jsp`, `angular`.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf("jsp", "angular"),
},
},
"voucher_customized": schema.BoolAttribute{
MarkdownDescription: "Whether vouchers are customized.",
Optional: true,
Computed: true,
},
"voucher_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable voucher-based authentication for guest access.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"wechat_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether WeChat authentication for guest access is enabled.",
Computed: true,
},
"wechat": schema.SingleNestedAttribute{
MarkdownDescription: "WeChat authentication settings.",
Optional: true,
Attributes: map[string]schema.Attribute{
"app_id": schema.StringAttribute{
MarkdownDescription: "WeChat App ID for social authentication.",
Required: true,
},
"app_secret": schema.StringAttribute{
MarkdownDescription: "WeChat App secret.",
Required: true,
Sensitive: true,
},
"secret_key": schema.StringAttribute{
MarkdownDescription: "WeChat secret key.",
Required: true,
Sensitive: true,
},
"shop_id": schema.StringAttribute{
MarkdownDescription: "WeChat Shop ID for payments.",
Optional: true,
Computed: true,
},
},
},
},
}
}
func NewGuestAccessResource() resource.Resource {
r := &guestAccessResource{}
r.GenericResource = NewSettingResource(
"unifi_setting_guest_access",
func() *guestAccessModel { return &guestAccessModel{} },
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
return client.GetSettingGuestAccess(ctx, site)
},
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
return client.UpdateSettingGuestAccess(ctx, site, body.(*unifi.SettingGuestAccess))
},
)
return r
}