Prevent incorrect usage of CNAME and DNAME record types
[regano.git] / db_api.sql
blobf83ef04771a2206a18afbc3e13307a2cb19383c4
1 -- Regano database function definitions
2 --
3 -- Uses PostgreSQL extensions.
4 --
5 -- These functions are intended to be called from the Web UI or other frontend.
6 --
7 --  Regano is a domain registration system for OpenNIC TLDs written in
8 --  Perl.  This file is part of Regano.
9 --
10 --  Regano may be distributed under the same terms as Perl itself.  Of
11 --  particular importance, note that while regano is distributed in the
12 --  hope that it will be useful, there is NO WARRANTY OF ANY KIND
13 --  WHATSOEVER WHETHER EXPLICIT OR IMPLIED.
16 -- The type definitions in db_types.sql must already be installed.
17 -- The table definitions in db_tables.sql must already be installed.
18 -- The function definitions in db_functions.sql must already be installed.
19 -- The configuration in db_config.sql must be loaded for these to actually work.
21 -- Inquire about the status of a domain.
22 CREATE OR REPLACE FUNCTION regano_api.domain_status
23         (regano.dns_fqdn)
24         RETURNS regano.domain_status AS $$
25 DECLARE
26     name                ALIAS FOR $1;
28     domain_term         CONSTANT interval NOT NULL
29                             := (regano.config_get('domain/term')).interval;
31     max_expired_age     CONSTANT interval NOT NULL
32                             := (regano.config_get('domain/grace_period')).interval;
33     max_pending_age     CONSTANT interval NOT NULL
34                             := (regano.config_get('domain/pend_term')).interval;
36     active_domain       regano.domains%ROWTYPE;
38     primary_label       regano.dns_label;
39     tail                regano.dns_fqdn;
40 BEGIN
41     PERFORM *
42         FROM regano.bailiwicks
43         WHERE lower(domain_tail) = lower(name)
44             OR lower(domain_tail) = '.'||lower(name);
45     IF FOUND THEN
46         RETURN 'BAILIWICK';
47     END IF;
49     primary_label := substring(name from '^([^.]+)[.]');
50     tail := substring(name from '^[^.]+([.].+[.])$');
52     PERFORM * FROM regano.bailiwicks WHERE lower(domain_tail) = lower(tail);
53     IF NOT FOUND THEN
54         RETURN 'ELSEWHERE';
55     END IF;
57     PERFORM * FROM regano.reserved_domains
58                 WHERE domain_name = lower(primary_label);
59     IF FOUND THEN
60         RETURN 'RESERVED';
61     END IF;
63     -- clean up pending domains, then check if the requested domain is pending
64     DELETE FROM regano.pending_domains WHERE start < (now() - max_pending_age);
65     PERFORM * FROM regano.pending_domains
66                 WHERE lower(domain_name) = lower(primary_label)
67                     AND lower(domain_tail) = lower(tail);
68     IF FOUND THEN
69         RETURN 'PENDING';
70     END IF;
72     -- automatically renew system domains
73     UPDATE regano.domains
74         SET expiration = now() + domain_term
75         WHERE domain_name IN ('@', '@@')
76             AND expiration < now();
78     -- clean up expired domains, then check if the requested domain is active
79     DELETE FROM regano.domains WHERE expiration < (now() - max_expired_age);
80     SELECT * INTO active_domain
81         FROM regano.domains
82         WHERE (lower(primary_label) = lower(domain_name))
83             AND (lower(tail) = lower(domain_tail));
85     IF FOUND THEN
86         IF now() < active_domain.expiration THEN
87             RETURN 'REGISTERED';
88         ELSE
89             RETURN 'EXPIRED';
90         END IF;
91     END IF;
93     RETURN 'AVAILABLE';
94 END;
95 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
96 ALTER FUNCTION regano_api.domain_status (regano.dns_fqdn)
97         OWNER TO regano;
99 -- Inquire why a domain is reserved.
100 CREATE OR REPLACE FUNCTION regano_api.domain_reservation_reason
101         (regano.dns_fqdn)
102         RETURNS text AS $$
103 SELECT CASE WHEN regano_api.domain_status($1) <> 'RESERVED' THEN NULL
104             ELSE reason END
105         FROM regano.reserved_domains
106         WHERE domain_name = substring($1 from '^([^.]+)[.]')
107 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
108 ALTER FUNCTION regano_api.domain_reservation_reason (regano.dns_fqdn)
109         OWNER TO regano;
111 -- Inquire how a domain is handled.
112 CREATE OR REPLACE FUNCTION regano_api.domain_mode
113         (regano.dns_fqdn)
114         RETURNS regano.domain_mode AS $$
115 DECLARE
116     name                ALIAS FOR $1;
118     active_domain       regano.domains%ROWTYPE;
119 BEGIN
120     SELECT * INTO active_domain
121         FROM regano.domains
122         WHERE lower(name) = lower(domain_name||domain_tail);
123     IF NOT FOUND OR now() > active_domain.expiration THEN
124         RETURN NULL;
125     END IF;
127     PERFORM * FROM regano.domain_records
128         WHERE domain_id = active_domain.id AND seq_no = 0 AND type = 'SOA';
129     IF FOUND THEN
130         RETURN 'HOSTED';
131     END IF;
133     PERFORM * FROM regano.domain_records r
134         WHERE domain_id = active_domain.id AND r.name = '@' AND type = 'NS';
135     IF FOUND THEN
136         RETURN 'DELEGATED';
137     END IF;
139     RETURN 'INLINE';
140 END;
141 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
142 ALTER FUNCTION regano_api.domain_mode (regano.dns_fqdn)
143         OWNER TO regano;
145 -- Inquire when a domain was last updated.
146 CREATE OR REPLACE FUNCTION regano_api.domain_last_update
147         (regano.dns_fqdn)
148         RETURNS timestamp with time zone AS $$
149 DECLARE
150     name                ALIAS FOR $1;
152     timestamp           timestamp with time zone;
153 BEGIN
154     SELECT last_update INTO timestamp
155         FROM regano.domains
156         WHERE lower(domain_name||domain_tail) = lower(name);
157     IF FOUND THEN
158         RETURN timestamp;
159     END IF;
161     PERFORM *
162         FROM regano.bailiwicks
163         WHERE lower(domain_tail) = lower(name)
164             OR lower(domain_tail) = '.'||lower(name);
165     IF FOUND THEN
166         SELECT MAX(last_update) INTO timestamp
167             FROM regano.domains
168             WHERE lower(domain_tail) = lower(name)
169                 OR lower(domain_tail) = '.'||lower(name);
170         RETURN timestamp;
171     END IF;
173     RETURN NULL;
175 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
176 ALTER FUNCTION regano_api.domain_last_update (regano.dns_fqdn)
177         OWNER TO regano;
180 -- Create a new user account.
181 CREATE OR REPLACE FUNCTION regano_api.user_register
182         (text, regano.password, text, text)
183         RETURNS void AS $$
184 DECLARE
185     username_           ALIAS FOR $1;
186     password_           ALIAS FOR $2;
187     contact_name        ALIAS FOR $3;
188     contact_email       ALIAS FOR $4;
190     crypt_alg           CONSTANT text NOT NULL
191                             := (regano.config_get('auth/crypt')).text;
192     crypt_iter          CONSTANT integer NOT NULL
193                             := (regano.config_get('auth/crypt')).number;
195     new_user_id         bigint; -- row ID of new user record
196 BEGIN
197     INSERT INTO users (username, password, contact_id)
198         VALUES (username_, ROW(password_.xdigest, password_.xsalt,
199                                 crypt(password_.digest,
200                                       gen_salt(crypt_alg, crypt_iter))), 1)
201         RETURNING id INTO STRICT new_user_id;
202     INSERT INTO contacts (owner_id, id, name, email)
203         VALUES (new_user_id, 1, contact_name, contact_email);
204 END;
205 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
206 ALTER FUNCTION regano_api.user_register (text, regano.password, text, text)
207         OWNER TO regano;
209 -- Get the external digest algorithm and salt for a user.
210 CREATE OR REPLACE FUNCTION regano_api.user_get_salt_info (text)
211         RETURNS regano.password AS $$
212 DECLARE
213     username_   ALIAS FOR $1;
215     password_   regano.password;
216 BEGIN
217     SELECT (password).xdigest, (password).xsalt INTO password_
218         FROM regano.users WHERE (username = username_);
219     IF NOT FOUND THEN
220         -- return an unspecified record to impede timing attacks
221         SELECT (password).xdigest, (password).xsalt INTO password_
222             FROM regano.users FETCH FIRST 1 ROW ONLY;
223     END IF;
225     RETURN password_;
226 END;
227 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
228 ALTER FUNCTION regano_api.user_get_salt_info (text)
229         OWNER TO regano;
231 -- Begin a session for a user.
232 CREATE OR REPLACE FUNCTION regano_api.user_login
233         (text, regano.password)
234         RETURNS uuid AS $$
235 <<var>>
236 DECLARE
237     username    ALIAS FOR $1;
238     password    ALIAS FOR $2;
240     crypt_alg   CONSTANT text NOT NULL
241                     := (regano.config_get('auth/crypt')).text;
242     crypt_iter  CONSTANT integer NOT NULL
243                     := (regano.config_get('auth/crypt')).number;
244     max_age     CONSTANT interval NOT NULL
245                     := (regano.config_get('session/max_age')).interval;
247     user_id     bigint; -- row ID of user record
248     stored_pw   text;   -- password hash from database
249     session_id  uuid;   -- session ID
250 BEGIN
251     SELECT id, (regano.users.password).digest INTO user_id, stored_pw
252         FROM regano.users WHERE (regano.users.username = var.username);
253     IF NOT FOUND OR stored_pw = '!' THEN
254         -- fake a stored password to impede timing attacks
255         stored_pw := gen_salt(crypt_alg, crypt_iter);
256     END IF;
257     -- clean up expired sessions
258     DELETE FROM regano.sessions WHERE start < (CURRENT_TIMESTAMP - max_age);
259     -- verify password; note that a bare salt cannot match any hash
260     IF crypt(password.digest, stored_pw) = stored_pw THEN
261         -- login successful
262         INSERT INTO regano.sessions (id, user_id)
263             VALUES (gen_random_uuid(), user_id)
264             RETURNING id INTO STRICT session_id;
265         RETURN session_id;
266     ELSE
267         -- login failed
268         RETURN NULL;
269     END IF;
270 END;
271 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
272 ALTER FUNCTION regano_api.user_login (text, regano.password)
273         OWNER TO regano;
275 -- Change a logged-in user's password.
276 CREATE OR REPLACE FUNCTION regano_api.user_change_password
277         (uuid, regano.password, regano.password)
278         RETURNS boolean AS $$
279 DECLARE
280     session_id  ALIAS FOR $1;
281     old_pw      ALIAS FOR $2;
282     new_pw      ALIAS FOR $3;
284     crypt_alg   CONSTANT text NOT NULL
285                     := (regano.config_get('auth/crypt')).text;
286     crypt_iter  CONSTANT integer NOT NULL
287                     := (regano.config_get('auth/crypt')).number;
289     user_id     bigint; -- row ID of user record
290 BEGIN
291     SELECT regano.sessions.user_id INTO user_id
292         FROM regano.sessions WHERE id = session_id;
293     IF NOT FOUND THEN
294         RETURN FALSE;
295     END IF;
297     new_pw.digest := crypt(new_pw.digest, gen_salt(crypt_alg, crypt_iter));
299     UPDATE regano.users
300         SET password = new_pw
301         WHERE ((id = user_id) AND
302             (crypt(old_pw.digest, (regano.users.password).digest) =
303              (regano.users.password).digest));
304     RETURN FOUND;
305 END;
306 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
307 ALTER FUNCTION regano_api.user_change_password
308         (uuid, regano.password, regano.password)
309         OWNER TO regano;
311 -- Recover a lost password and open a session.
312 CREATE OR REPLACE FUNCTION regano_api.user_recover_password
313         (uuid, uuid, regano.password)
314         RETURNS uuid AS $$
315 DECLARE
316     verification_id     ALIAS FOR $1;
317     verification_key    ALIAS FOR $2;
318     new_pw              ALIAS FOR $3;
320     crypt_alg   CONSTANT text NOT NULL
321                     := (regano.config_get('auth/crypt')).text;
322     crypt_iter  CONSTANT integer NOT NULL
323                     := (regano.config_get('auth/crypt')).number;
325     verification        regano.contact_verifications%ROWTYPE;
326     session_id          uuid;
327 BEGIN
328     SELECT * INTO verification
329         FROM regano.contact_verifications
330         WHERE type = 'account_recovery'
331                 AND id = verification_id AND key = verification_key;
332     IF NOT FOUND THEN
333         RETURN NULL;
334     END IF;
336     new_pw.digest := crypt(new_pw.digest, gen_salt(crypt_alg, crypt_iter));
338     UPDATE regano.users
339         SET password = new_pw
340         WHERE id = verification.user_id;
341     UPDATE regano.contacts
342         SET email_verified = TRUE
343         WHERE owner_id = verification.user_id AND id = verification.contact_id;
345     INSERT INTO regano.sessions (id, user_id)
346         VALUES (gen_random_uuid(), verification.user_id)
347         RETURNING id INTO STRICT session_id;
349     DELETE
350         FROM regano.contact_verifications
351         WHERE id = verification_id;
352     RETURN session_id;
353 END;
354 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
355 ALTER FUNCTION regano_api.user_recover_password
356         (uuid, uuid, regano.password)
357         OWNER TO regano;
359 -- End a session.
360 CREATE OR REPLACE FUNCTION regano_api.session_logout (uuid) RETURNS void AS $$
361 DELETE FROM regano.sessions WHERE id = $1
362 $$ LANGUAGE SQL VOLATILE STRICT SECURITY DEFINER;
363 ALTER FUNCTION regano_api.session_logout (uuid)
364         OWNER TO regano;
366 -- Retrieve username for a session, update session activity timestamp, and
367 -- perform auto-logout if the session has expired.
368 CREATE OR REPLACE FUNCTION regano_api.session_check (uuid)
369         RETURNS text AS $$
370 <<var>>
371 DECLARE
372     id          ALIAS FOR $1;
374     max_age     CONSTANT interval NOT NULL
375                     := (regano.config_get('session/max_age')).interval;
376     max_idle    CONSTANT interval NOT NULL
377                     := (regano.config_get('session/max_idle')).interval;
379     session     regano.sessions%ROWTYPE;
380 BEGIN
381     SELECT * INTO session
382         FROM regano.sessions WHERE regano.sessions.id = var.id;
383     IF FOUND THEN
384         IF ((CURRENT_TIMESTAMP - session.activity) > max_idle) OR
385            ((CURRENT_TIMESTAMP - session.start) > max_age) THEN
386             -- session is expired
387             PERFORM regano_api.session_logout(session.id);
388             RETURN NULL;
389         ELSIF ((CURRENT_TIMESTAMP - session.activity) * 4 > max_idle) THEN
390             -- update activity timestamp
391             UPDATE regano.sessions
392                 SET activity = CURRENT_TIMESTAMP
393                 WHERE regano.sessions.id = var.id;
394         END IF;
395     ELSE
396         -- no such session exists
397         RETURN NULL;
398     END IF;
399     RETURN regano.username(session);
400 END;
401 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
402 ALTER FUNCTION regano_api.session_check (uuid)
403         OWNER TO regano;
405 -- Retrieve the current user's user record, sans password.
406 CREATE OR REPLACE FUNCTION regano_api.user_info
407         (session_id uuid)
408         RETURNS regano.users AS $$
409 DECLARE
410     user        regano.users%ROWTYPE;
411 BEGIN
412     SELECT * INTO STRICT user
413         FROM regano.users WHERE id = regano.session_user_id(session_id);
414     user.password := NULL;
415     RETURN user;
416 END;
417 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
418 ALTER FUNCTION regano_api.user_info (uuid)
419         OWNER TO regano;
421 -- Retrieve the ID of the current user's primary contact record.
422 CREATE OR REPLACE FUNCTION regano_api.contact_primary_id
423         (session_id uuid)
424         RETURNS integer AS $$
425 SELECT contact_id FROM regano.users WHERE id = regano.session_user_id($1)
426 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
427 ALTER FUNCTION regano_api.contact_primary_id (uuid)
428         OWNER TO regano;
430 -- Change the primary contact for the current user.
431 CREATE OR REPLACE FUNCTION regano_api.user_set_primary_contact
432         (session_id uuid, contact_id integer)
433         RETURNS void AS $$
434 DECLARE
435     domain_term         CONSTANT interval NOT NULL
436                             := (regano.config_get('domain/term')).interval;
438     contact             regano.contacts%ROWTYPE;
439     session             regano.sessions%ROWTYPE;
440     pending_domain      regano.pending_domains%ROWTYPE;
441 BEGIN
442     SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
443     SELECT * INTO STRICT contact FROM regano.contacts
444         WHERE owner_id = session.user_id AND id = contact_id;
446     IF NOT contact.email_verified THEN
447         RAISE EXCEPTION
448         'Only a verified email address may be set as primary contact.';
449     END IF;
450     -- update user record
451     UPDATE regano.users
452         SET contact_id = contact.id
453         WHERE id = session.user_id;
454     -- check for a pending domain
455     SELECT * INTO pending_domain
456         FROM regano.pending_domains
457         WHERE pending_domains.user_id = session.user_id;
458     IF FOUND THEN
459         -- register the pending domain
460         DELETE
461             FROM regano.pending_domains
462             WHERE domain_name = pending_domain.domain_name
463                 AND domain_tail = pending_domain.domain_tail;
464         INSERT INTO regano.domains
465             (domain_name, domain_tail, owner_id, expiration)
466             VALUES (pending_domain.domain_name, pending_domain.domain_tail,
467                     session.user_id, now() + domain_term);
468     END IF;
469 END;
470 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
471 ALTER FUNCTION regano_api.user_set_primary_contact (uuid, integer)
472         OWNER TO regano;
474 -- Retrieve all contact records belonging to the current user.
475 CREATE OR REPLACE FUNCTION regano_api.contact_list
476         (session_id uuid)
477         RETURNS SETOF regano.contacts AS $$
478 SELECT * FROM regano.contacts WHERE owner_id = regano.session_user_id($1)
479 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
480 ALTER FUNCTION regano_api.contact_list (uuid)
481         OWNER TO regano;
483 -- Add a contact record for the current user.
484 CREATE OR REPLACE FUNCTION regano_api.contact_add
485         (session_id uuid, name text, email text)
486         RETURNS integer AS $$
487 INSERT INTO regano.contacts (owner_id, id, name, email)
488     VALUES (regano.session_user_id($1),
489             regano.contact_next_id(regano.session_user_id($1)), $2, $3)
490     RETURNING id;
491 $$ LANGUAGE SQL VOLATILE STRICT SECURITY DEFINER;
492 ALTER FUNCTION regano_api.contact_add (uuid, text, text)
493         OWNER TO regano;
495 -- Remove a contact record for the current user.
496 CREATE OR REPLACE FUNCTION regano_api.contact_remove
497         (session_id uuid, contact_id integer)
498         RETURNS void AS $$
499 DECLARE
500     user_id     CONSTANT bigint NOT NULL
501                     := regano.session_user_id(session_id);
502     renumbering CURSOR (user_id bigint)
503                     FOR SELECT id
504                             FROM regano.contacts
505                             WHERE owner_id = user_id
506                             ORDER BY id
507                             FOR UPDATE;
508     i           integer := 0;
509 BEGIN
510     DELETE
511         FROM regano.contacts
512         WHERE owner_id = user_id AND id = contact_id;
513     FOR contact IN renumbering (user_id) LOOP
514         i := i + 1;
515         UPDATE regano.contacts
516             SET id = i
517             WHERE CURRENT OF renumbering;
518     END LOOP;
520 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
521 ALTER FUNCTION regano_api.contact_remove (uuid, integer)
522         OWNER TO regano;
524 -- Update the name field of a contact record.
525 CREATE OR REPLACE FUNCTION regano_api.contact_update_name
526         (session_id uuid, contact_id integer, value text)
527         RETURNS void AS $$
528 DECLARE
529     contact     regano.contacts%ROWTYPE;
530     session     regano.sessions%ROWTYPE;
531 BEGIN
532     SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
533     SELECT * INTO STRICT contact FROM regano.contacts
534         WHERE owner_id = session.user_id AND id = contact_id
535         FOR UPDATE;
537     UPDATE regano.contacts
538         SET name = value
539         WHERE owner_id = session.user_id AND id = contact_id;
540 END;
541 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
542 ALTER FUNCTION regano_api.contact_update_name (uuid, integer, text)
543         OWNER TO regano;
545 -- Update the email address field of a contact record.
546 CREATE OR REPLACE FUNCTION regano_api.contact_update_email
547         (session_id uuid, contact_id integer, value text)
548         RETURNS void AS $$
549 DECLARE
550     contact             regano.contacts%ROWTYPE;
551     session             regano.sessions%ROWTYPE;
553     primary_contact_id  integer;
554 BEGIN
555     SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
556     SELECT users.contact_id INTO STRICT primary_contact_id
557         FROM regano.users WHERE id = session.user_id;
558     SELECT * INTO STRICT contact FROM regano.contacts
559         WHERE owner_id = session.user_id AND id = contact_id
560         FOR UPDATE;
562     IF contact_id = primary_contact_id AND contact.email_verified THEN
563         RAISE EXCEPTION
564         'Verified email address (%) for primary contact (%) may not be changed.',
565             contact.email, contact_id;
566     END IF;
568     -- cancel any in-progress address verification
569     DELETE FROM regano.contact_verifications
570         WHERE contact_verifications.user_id = session.user_id
571           AND contact_verifications.contact_id = contact.id;
572     -- change the stored email address
573     UPDATE regano.contacts
574         SET email_verified = FALSE, email = value
575         WHERE owner_id = session.user_id AND id = contact_id;
576 END;
577 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
578 ALTER FUNCTION regano_api.contact_update_email (uuid, integer, text)
579         OWNER TO regano;
581 -- Update the PGP key field of a contact record
582 CREATE OR REPLACE FUNCTION regano_api.contact_update_pgp_key
583         (session_id uuid, contact_id integer, value text)
584         RETURNS void AS $$
585 DECLARE
586     contact     regano.contacts%ROWTYPE;
587     session     regano.sessions%ROWTYPE;
588 BEGIN
589     SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
590     SELECT * INTO STRICT contact FROM regano.contacts
591         WHERE owner_id = session.user_id AND id = contact_id
592         FOR UPDATE;
595     IF char_length(value) > 0 THEN
596         UPDATE regano.contacts
597             SET pgp_key = value, pgp_key_id = pgp_key_id(dearmor(value))
598             WHERE owner_id = session.user_id AND id = contact_id;
599     ELSIF value IS NULL OR char_length(value) = 0 THEN
600         UPDATE regano.contacts
601             SET pgp_key = NULL, pgp_key_id = NULL
602             WHERE owner_id = session.user_id AND id = contact_id;
603     END IF;
604 END;
605 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
606 ALTER FUNCTION regano_api.contact_update_pgp_key (uuid, integer, text)
607         OWNER TO regano;
610 -- Begin the process of verifying a contact record.
611 CREATE OR REPLACE FUNCTION regano_api.contact_verify_begin
612         (session_id uuid, contact_id integer)
613         RETURNS void AS $$
614 DECLARE
615     contact     regano.contacts%ROWTYPE;
616     session     regano.sessions%ROWTYPE;
617 BEGIN
618     SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
619     SELECT * INTO STRICT contact FROM regano.contacts
620         WHERE owner_id = session.user_id AND id = contact_id;
622     DELETE FROM regano.contact_verifications
623         WHERE contact_verifications.contact_id = contact_verify_begin.contact_id
624           AND contact_verifications.user_id = session.user_id;
625     INSERT INTO regano.contact_verifications (id, key, type, user_id, contact_id)
626         VALUES (gen_random_uuid(), gen_random_uuid(), 'email_address',
627                 session.user_id, contact_id);
628     NOTIFY regano__contact_verifications;
629 END;
630 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
631 ALTER FUNCTION regano_api.contact_verify_begin (uuid, integer)
632         OWNER TO regano;
634 -- Complete verification of a contact record.
635 CREATE OR REPLACE FUNCTION regano_api.contact_verify_complete
636         (verification_id uuid, key uuid)
637         RETURNS boolean AS $$
638 <<var>>
639 DECLARE
640     domain_term         CONSTANT interval NOT NULL
641                             := (regano.config_get('domain/term')).interval;
642     max_age             CONSTANT interval NOT NULL
643                             := (regano.config_get('verify/max_age')).interval;
645     pending_domain      regano.pending_domains%ROWTYPE;
646     verification        regano.contact_verifications%ROWTYPE;
648     is_primary_contact  boolean;
649 BEGIN
650     -- clean up expired verifications
651     DELETE
652         FROM regano.contact_verifications
653         WHERE start < (CURRENT_TIMESTAMP - max_age);
654     -- look up the provided verification ID
655     SELECT * INTO verification
656         FROM regano.contact_verifications
657         WHERE (type = 'email_address') AND (id = verification_id) AND
658               (contact_verifications.key = contact_verify_complete.key);
659     IF NOT FOUND THEN
660         RETURN FALSE;
661     END IF;
662     -- mark email address as verified
663     UPDATE regano.contacts
664         SET email_verified = TRUE
665         WHERE owner_id = verification.user_id AND id = verification.contact_id;
666     -- check if a primary contact was verified
667     SELECT users.contact_id = verification.contact_id
668         INTO STRICT is_primary_contact
669         FROM regano.users
670         WHERE id = verification.user_id;
671     -- check for a pending domain
672     SELECT * INTO pending_domain
673         FROM regano.pending_domains
674         WHERE pending_domains.user_id = verification.user_id;
675     IF FOUND AND is_primary_contact THEN
676         -- register the pending domain
677         DELETE
678             FROM regano.pending_domains
679             WHERE domain_name = pending_domain.domain_name
680                 AND domain_tail = pending_domain.domain_tail;
681         INSERT INTO regano.domains
682             (domain_name, domain_tail, owner_id, expiration)
683             VALUES (pending_domain.domain_name, pending_domain.domain_tail,
684                     verification.user_id, now() + domain_term);
685     END IF;
686     -- clean up the successful verification
687     DELETE
688         FROM regano.contact_verifications
689         WHERE id = verification_id;
690     RETURN TRUE;
691 END;
692 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
693 ALTER FUNCTION regano_api.contact_verify_complete (uuid, uuid)
694         OWNER TO regano;
697 -- Retrieve information about a pending domain belonging to the current user.
698 CREATE OR REPLACE FUNCTION regano_api.domain_check_pending
699         (uuid)
700         RETURNS regano.pending_domain AS $$
701 SELECT  domain_name||domain_tail AS name,
702         start, start + regano.config_get('domain/pend_term') AS expire
703     FROM regano.pending_domains WHERE user_id = regano.session_user_id($1)
704 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
705 ALTER FUNCTION regano_api.domain_check_pending (uuid)
706         OWNER TO regano;
708 -- Retrieve all domains belonging to the current user.
709 -- The domain table is public; this is for the account overview page.
710 CREATE OR REPLACE FUNCTION regano_api.domain_list
711         (uuid)
712         RETURNS SETOF regano.domain AS $$
713 SELECT domain_name||domain_tail AS name, registered, expiration, last_update,
714         CASE WHEN now() < expiration
715              THEN 'REGISTERED'::regano.domain_status
716              ELSE 'EXPIRED'::regano.domain_status
717         END AS status
718     FROM regano.domains WHERE owner_id = regano.session_user_id($1)
719 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
720 ALTER FUNCTION regano_api.domain_list (uuid)
721         OWNER TO regano;
723 -- Register an available domain.
724 CREATE OR REPLACE FUNCTION regano_api.domain_register
725         (uuid, regano.dns_fqdn)
726         RETURNS regano.domain_status AS $$
727 <<var>>
728 DECLARE
729     session_id          ALIAS FOR $1;
730     name                ALIAS FOR $2;
732     domain_term         CONSTANT interval NOT NULL
733                             := (regano.config_get('domain/term')).interval;
735     user_id             CONSTANT bigint NOT NULL
736                             := regano.session_user_id(session_id);
738     verified            boolean;        -- verified email address on file?
740     primary_label       regano.dns_label;
741     tail                regano.dns_fqdn;
742 BEGIN
743     primary_label := substring(name from '^([^.]+)[.]');
744     tail := substring(name from '^[^.]+([.].+[.])$');
746     IF regano_api.domain_status(name) <> 'AVAILABLE' THEN
747         RETURN regano_api.domain_status(name);
748     END IF;
750     SELECT email_verified INTO STRICT verified
751         FROM regano.users JOIN regano.contacts
752             ON owner_id = user_id AND contact_id = contacts.id
753         WHERE regano.users.id = user_id;
755     IF verified THEN
756         -- user has a verified email address; register the domain now
757         INSERT INTO regano.domains
758             (domain_name, domain_tail, owner_id, expiration)
759             VALUES (primary_label, tail, user_id, now() + domain_term);
760         RETURN 'REGISTERED';
761     ELSE
762         -- no verified email address on file; registration will be pending
763         INSERT INTO regano.pending_domains
764             (domain_name, domain_tail, user_id)
765             VALUES (primary_label, tail, user_id);
766         RETURN 'PENDING';
767     END IF;
768 END;
769 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
770 ALTER FUNCTION regano_api.domain_register (uuid, regano.dns_fqdn)
771         OWNER TO regano;
773 -- Renew a domain
774 CREATE OR REPLACE FUNCTION regano_api.domain_renew
775         (uuid, regano.dns_fqdn)
776         RETURNS timestamp with time zone AS $$
777 DECLARE
778     session_id          ALIAS FOR $1;
779     name                ALIAS FOR $2;
781     domain_term         CONSTANT interval NOT NULL
782                             := (regano.config_get('domain/term')).interval;
784     user_id             CONSTANT bigint NOT NULL
785                             := regano.session_user_id(session_id);
787     primary_label       regano.dns_label;
788     tail                regano.dns_fqdn;
790     domain              regano.domains%ROWTYPE;
791     result              timestamp with time zone;
792 BEGIN
793     primary_label := substring(name from '^([^.]+)[.]');
794     tail := substring(name from '^[^.]+([.].+[.])$');
796     SELECT * INTO STRICT domain
797         FROM regano.domains
798         WHERE (lower(primary_label) = lower(domain_name))
799             AND (lower(tail) = lower(domain_tail))
800         FOR UPDATE;
802     IF user_id <> domain.owner_id THEN
803         RAISE EXCEPTION
804         'attempt made to renew domain (%) not belonging to current user (%)',
805             name, regano.username(session_id);
806     END IF;
808     UPDATE regano.domains
809         SET expiration = now() + domain_term,
810             last_update = now()
811         WHERE id = domain.id
812         RETURNING expiration INTO STRICT result;
813     RETURN result;
815 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
816 ALTER FUNCTION regano_api.domain_renew (uuid, regano.dns_fqdn)
817         OWNER TO regano;
819 -- Immediately expire a domain
820 CREATE OR REPLACE FUNCTION regano_api.domain_release
821         (uuid, regano.dns_fqdn)
822         RETURNS void AS $$
823 DECLARE
824     session_id          ALIAS FOR $1;
825     name                ALIAS FOR $2;
827     user_id             CONSTANT bigint NOT NULL
828                             := regano.session_user_id(session_id);
830     primary_label       regano.dns_label;
831     tail                regano.dns_fqdn;
833     domain              regano.domains%ROWTYPE;
834 BEGIN
835     primary_label := substring(name from '^([^.]+)[.]');
836     tail := substring(name from '^[^.]+([.].+[.])$');
838     PERFORM *
839         FROM regano.reserved_domains
840         WHERE (domain_name = lower(primary_label));
841     IF FOUND THEN
842         RETURN;
843     END IF;
845     SELECT * INTO STRICT domain
846         FROM regano.domains
847         WHERE (lower(primary_label) = lower(domain_name))
848             AND (lower(tail) = lower(domain_tail))
849         FOR UPDATE;
851     IF user_id <> domain.owner_id THEN
852         RAISE EXCEPTION
853         'attempt made to release domain (%) not belonging to current user (%)',
854             name, regano.username(session_id);
855     END IF;
857     UPDATE regano.domains
858         SET expiration = now(),
859             last_update = now()
860         WHERE id = domain.id;
862 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
863 ALTER FUNCTION regano_api.domain_release (uuid, regano.dns_fqdn)
864         OWNER TO regano;
866 -- Set default TTL for records in a domain
867 CREATE OR REPLACE FUNCTION regano_api.domain_set_default_ttl
868         (uuid, regano.dns_fqdn, regano.dns_interval)
869         RETURNS void AS $$
870 DECLARE
871     session_id          ALIAS FOR $1;
872     name                ALIAS FOR $2;
873     new_ttl             ALIAS FOR $3;
875     user_id             CONSTANT bigint NOT NULL
876                             := regano.session_user_id(session_id);
878     primary_label       regano.dns_label;
879     tail                regano.dns_fqdn;
881     domain              regano.domains%ROWTYPE;
882 BEGIN
883     primary_label := substring(name from '^([^.]+)[.]');
884     tail := substring(name from '^[^.]+([.].+[.])$');
886     SELECT * INTO STRICT domain
887         FROM regano.domains
888         WHERE (lower(primary_label) = lower(domain_name))
889             AND (lower(tail) = lower(domain_tail))
890         FOR UPDATE;
892     IF user_id <> domain.owner_id THEN
893         RAISE EXCEPTION
894         'attempt made to set TTL for domain (%) not belonging to current user (%)',
895             name, regano.username(session_id);
896     END IF;
898     UPDATE regano.domains
899         SET default_ttl = new_ttl,
900             last_update = now()
901         WHERE id = domain.id;
903 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
904 ALTER FUNCTION regano_api.domain_set_default_ttl
905         (uuid, regano.dns_fqdn, regano.dns_interval)
906         OWNER TO regano;
908 -- Updating domain records is done in multiple steps, all in a single
909 -- database transaction.  First, the existing records for the domain are
910 -- removed.  Second, new records are inserted in order.  Third, the
911 -- database transaction is committed.
913 -- Remove existing records for a domain
914 CREATE OR REPLACE FUNCTION regano_api.zone_clear
915         (session_id     uuid,
916          zone_name      regano.dns_fqdn)
917         RETURNS void AS $$
918 DECLARE
919     domain              regano.domains%ROWTYPE;
920 BEGIN
921     domain := regano.zone_verify_access(session_id, zone_name, 'clear zone');
923     DELETE
924         FROM regano.domain_records
925         WHERE domain_id = domain.id;
926     UPDATE regano.domains
927         SET last_update = now()
928         WHERE id = domain.id;
929     NOTIFY regano__domain_records;
931 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
932 ALTER FUNCTION regano_api.zone_clear (uuid, regano.dns_fqdn)
933         OWNER TO regano;
935 -- Add an SOA record for a domain
936 -- NOTE: A domain may only have one SOA record, at the domain root, with
937 -- sequence number zero.
938 CREATE OR REPLACE FUNCTION regano_api.zone_add_SOA
939         (session_id     uuid,
940          zone_name      regano.dns_fqdn,
941          rec_ttl        regano.dns_interval,
942          SOA_master     regano.dns_fqdn,
943          SOA_mbox       regano.dns_email,
944          SOA_refresh    regano.dns_interval,
945          SOA_retry      regano.dns_interval,
946          SOA_expire     regano.dns_interval,
947          SOA_minimum    regano.dns_interval)
948         RETURNS void AS $$
949 DECLARE
950     domain              regano.domains%ROWTYPE;
951 BEGIN
952     domain := regano.zone_verify_access(session_id, zone_name, 'add SOA');
954     INSERT INTO regano.domain_records
955         (domain_id, seq_no, type, name, ttl, data_RR_SOA)
956         VALUES (domain.id, 0, 'SOA', '@', rec_ttl,
957                 ROW(SOA_master, SOA_mbox, SOA_refresh, SOA_retry,
958                     SOA_expire, SOA_minimum));
959     UPDATE regano.domains
960         SET last_update = now()
961         WHERE id = domain.id;
962     PERFORM regano.zone_verify_records(domain);
964 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
965 ALTER FUNCTION regano_api.zone_add_SOA
966         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_fqdn,
967          regano.dns_email, regano.dns_interval, regano.dns_interval,
968          regano.dns_interval, regano.dns_interval)
969         OWNER TO regano;
971 -- Add a record that stores another DNS name
972 CREATE OR REPLACE FUNCTION regano_api.zone_add_name
973         (session_id     uuid,
974          zone_name      regano.dns_fqdn,
975          rec_ttl        regano.dns_interval,
976          rec_name       regano.dns_name,
977          rec_type       regano.dns_record_type,
978          rec_data       regano.dns_name)
979         RETURNS void AS $$
980 DECLARE
981     domain              regano.domains%ROWTYPE;
982     new_seq_no          bigint;
983     rec_name_c          CONSTANT regano.dns_name NOT NULL
984                             := regano.canonicalize_record_name(rec_name,
985                                                                zone_name);
986 BEGIN
987     domain := regano.zone_verify_access(session_id, zone_name, 'add name');
988     new_seq_no := regano.zone_next_seq_no(domain.id);
990     INSERT INTO regano.domain_records
991         (domain_id, seq_no, type, name, ttl, data_name)
992         VALUES (domain.id, new_seq_no, rec_type, rec_name_c, rec_ttl, rec_data);
993     UPDATE regano.domains
994         SET last_update = now()
995         WHERE id = domain.id;
996     PERFORM regano.zone_verify_records(domain);
998 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
999 ALTER FUNCTION regano_api.zone_add_name
1000         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1001          regano.dns_record_type, regano.dns_name)
1002         OWNER TO regano;
1004 -- Add a record that stores free-form text
1005 CREATE OR REPLACE FUNCTION regano_api.zone_add_text
1006         (session_id     uuid,
1007          zone_name      regano.dns_fqdn,
1008          rec_ttl        regano.dns_interval,
1009          rec_name       regano.dns_name,
1010          rec_type       regano.dns_record_type,
1011          rec_data       text)
1012         RETURNS void AS $$
1013 DECLARE
1014     domain              regano.domains%ROWTYPE;
1015     new_seq_no          bigint;
1016     rec_name_c          CONSTANT regano.dns_name NOT NULL
1017                             := regano.canonicalize_record_name(rec_name,
1018                                                                zone_name);
1019 BEGIN
1020     domain := regano.zone_verify_access(session_id, zone_name, 'add text');
1021     new_seq_no := regano.zone_next_seq_no(domain.id);
1023     INSERT INTO regano.domain_records
1024         (domain_id, seq_no, type, name, ttl, data_text)
1025         VALUES (domain.id, new_seq_no, rec_type, rec_name_c, rec_ttl, rec_data);
1026     UPDATE regano.domains
1027         SET last_update = now()
1028         WHERE id = domain.id;
1029     PERFORM regano.zone_verify_records(domain);
1031 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1032 ALTER FUNCTION regano_api.zone_add_text
1033         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1034          regano.dns_record_type, text)
1035         OWNER TO regano;
1037 -- Add an A record
1038 CREATE OR REPLACE FUNCTION regano_api.zone_add_A
1039         (session_id     uuid,
1040          zone_name      regano.dns_fqdn,
1041          rec_ttl        regano.dns_interval,
1042          rec_name       regano.dns_name,
1043          rec_data       regano.dns_RR_A)
1044         RETURNS void AS $$
1045 DECLARE
1046     domain              regano.domains%ROWTYPE;
1047     new_seq_no          bigint;
1048     rec_name_c          CONSTANT regano.dns_name NOT NULL
1049                             := regano.canonicalize_record_name(rec_name,
1050                                                                zone_name);
1051 BEGIN
1052     domain := regano.zone_verify_access(session_id, zone_name, 'add A');
1053     new_seq_no := regano.zone_next_seq_no(domain.id);
1055     INSERT INTO regano.domain_records
1056         (domain_id, seq_no, type, name, ttl, data_RR_A)
1057         VALUES (domain.id, new_seq_no, 'A', rec_name_c, rec_ttl, rec_data);
1058     UPDATE regano.domains
1059         SET last_update = now()
1060         WHERE id = domain.id;
1061     PERFORM regano.zone_verify_records(domain);
1063 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1064 ALTER FUNCTION regano_api.zone_add_A
1065         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1066          regano.dns_RR_A)
1067         OWNER TO regano;
1069 -- Add an AAAA record
1070 CREATE OR REPLACE FUNCTION regano_api.zone_add_AAAA
1071         (session_id     uuid,
1072          zone_name      regano.dns_fqdn,
1073          rec_ttl        regano.dns_interval,
1074          rec_name       regano.dns_name,
1075          rec_data       regano.dns_RR_AAAA)
1076         RETURNS void AS $$
1077 DECLARE
1078     domain              regano.domains%ROWTYPE;
1079     new_seq_no          bigint;
1080     rec_name_c          CONSTANT regano.dns_name NOT NULL
1081                             := regano.canonicalize_record_name(rec_name,
1082                                                                zone_name);
1083 BEGIN
1084     domain := regano.zone_verify_access(session_id, zone_name, 'add AAAA');
1085     new_seq_no := regano.zone_next_seq_no(domain.id);
1087     INSERT INTO regano.domain_records
1088         (domain_id, seq_no, type, name, ttl, data_RR_AAAA)
1089         VALUES (domain.id, new_seq_no, 'AAAA', rec_name_c, rec_ttl, rec_data);
1090     UPDATE regano.domains
1091         SET last_update = now()
1092         WHERE id = domain.id;
1093     PERFORM regano.zone_verify_records(domain);
1095 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1096 ALTER FUNCTION regano_api.zone_add_AAAA
1097         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1098          regano.dns_RR_AAAA)
1099         OWNER TO regano;
1101 -- Add a DS record
1102 CREATE OR REPLACE FUNCTION regano_api.zone_add_DS
1103         (session_id     uuid,
1104          zone_name      regano.dns_fqdn,
1105          rec_ttl        regano.dns_interval,
1106          rec_name       regano.dns_name,
1107          DS_key_tag     regano.uint16bit,
1108          DS_algorithm   regano.uint8bit,
1109          DS_digest_type regano.uint8bit,
1110          DS_digest      regano.hexstring)
1111         RETURNS void AS $$
1112 DECLARE
1113     domain              regano.domains%ROWTYPE;
1114     new_seq_no          bigint;
1115     rec_name_c          CONSTANT regano.dns_name NOT NULL
1116                             := regano.canonicalize_record_name(rec_name,
1117                                                                zone_name);
1118 BEGIN
1119     domain := regano.zone_verify_access(session_id, zone_name, 'add DS');
1120     new_seq_no := regano.zone_next_seq_no(domain.id);
1122     INSERT INTO regano.domain_records
1123         (domain_id, seq_no, type, name, ttl, data_RR_DS)
1124         VALUES (domain.id, new_seq_no, 'DS', rec_name_c, rec_ttl,
1125                 ROW(DS_key_tag, DS_algorithm, DS_digest_type, DS_digest));
1126     UPDATE regano.domains
1127         SET last_update = now()
1128         WHERE id = domain.id;
1129     PERFORM regano.zone_verify_records(domain);
1131 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1132 ALTER FUNCTION regano_api.zone_add_DS
1133         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1134          regano.uint16bit, regano.uint8bit, regano.uint8bit,
1135          regano.hexstring)
1136         OWNER TO regano;
1138 -- Add an MX record
1139 CREATE OR REPLACE FUNCTION regano_api.zone_add_MX
1140         (session_id     uuid,
1141          zone_name      regano.dns_fqdn,
1142          rec_ttl        regano.dns_interval,
1143          rec_name       regano.dns_name,
1144          MX_preference  regano.uint16bit,
1145          MX_exchange    regano.dns_name)
1146         RETURNS void AS $$
1147 DECLARE
1148     domain              regano.domains%ROWTYPE;
1149     new_seq_no          bigint;
1150     rec_name_c          CONSTANT regano.dns_name NOT NULL
1151                             := regano.canonicalize_record_name(rec_name,
1152                                                                zone_name);
1153 BEGIN
1154     domain := regano.zone_verify_access(session_id, zone_name, 'add MX');
1155     new_seq_no := regano.zone_next_seq_no(domain.id);
1157     INSERT INTO regano.domain_records
1158         (domain_id, seq_no, type, name, ttl, data_RR_MX)
1159         VALUES (domain.id, new_seq_no, 'MX', rec_name_c, rec_ttl,
1160                 ROW(MX_preference, MX_exchange));
1161     UPDATE regano.domains
1162         SET last_update = now()
1163         WHERE id = domain.id;
1164     PERFORM regano.zone_verify_records(domain);
1166 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1167 ALTER FUNCTION regano_api.zone_add_MX
1168         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1169          regano.uint16bit, regano.dns_name)
1170         OWNER TO regano;
1172 -- Add a SRV record
1173 CREATE OR REPLACE FUNCTION regano_api.zone_add_SRV
1174         (session_id     uuid,
1175          zone_name      regano.dns_fqdn,
1176          rec_ttl        regano.dns_interval,
1177          rec_name       regano.dns_name,
1178          SRV_priority   regano.uint16bit,
1179          SRV_weight     regano.uint16bit,
1180          SRV_port       regano.uint16bit,
1181          SRV_target     regano.dns_fqdn)
1182         RETURNS void AS $$
1183 DECLARE
1184     domain              regano.domains%ROWTYPE;
1185     new_seq_no          bigint;
1186     rec_name_c          CONSTANT regano.dns_name NOT NULL
1187                             := regano.canonicalize_record_name(rec_name,
1188                                                                zone_name);
1189 BEGIN
1190     domain := regano.zone_verify_access(session_id, zone_name, 'add SRV');
1191     new_seq_no := regano.zone_next_seq_no(domain.id);
1193     INSERT INTO regano.domain_records
1194         (domain_id, seq_no, type, name, ttl, data_RR_SRV)
1195         VALUES (domain.id, new_seq_no, 'SRV', rec_name_c, rec_ttl,
1196                 ROW(SRV_priority, SRV_weight, SRV_port, SRV_target));
1197     UPDATE regano.domains
1198         SET last_update = now()
1199         WHERE id = domain.id;
1200     PERFORM regano.zone_verify_records(domain);
1202 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1203 ALTER FUNCTION regano_api.zone_add_SRV
1204         (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1205          regano.uint16bit, regano.uint16bit, regano.uint16bit,
1206          regano.dns_fqdn)
1207         OWNER TO regano;